UNPKG

@hebcal/core

Version:

A perpetual Jewish Calendar API

585 lines (582 loc) 23 kB
/*! @hebcal/core v5.10.1, distributed under GPLv2 https://www.gnu.org/licenses/gpl-2.0.txt */ import 'temporal-polyfill/global'; import { NOAACalculator } from '@hebcal/noaa'; import { getTimezoneOffset, pad2, getPseudoISO, HDate, isDate } from '@hebcal/hdate'; /** * @private */ function zdtToDate(zdt) { if (zdt === null) { return new Date(NaN); } const res = new Date(zdt.epochMilliseconds); res.setMilliseconds(0); return res; } function getDate(date) { if (isDate(date)) return date; if (HDate.isHDate(date)) return date.greg(); throw new TypeError(`invalid date: ${date}`); } /** * Calculate halachic times (zmanim / זְמַנִּים) for a given day and location. * Calculations are available for tzeit / tzais (nightfall), * shkiah (sunset) and more. * * Zmanim are estimated using an algorithm published by the US National Oceanic * and Atmospheric Administration. The NOAA solar calculator is based on equations * from _Astronomical Algorithms_ by Jean Meeus. * * The sunrise and sunset results are theoretically accurate to within a minute for * locations between +/- 72° latitude, and within 10 minutes outside of those latitudes. * However, due to variations in atmospheric composition, temperature, pressure and * conditions, observed values may vary from calculations. * https://gml.noaa.gov/grad/solcalc/calcdetails.html * * @example * const {GeoLocation, Zmanim} = require('@hebcal/core'); * const latitude = 41.822232; * const longitude = -71.448292; * const tzid = 'America/New_York'; * const friday = new Date(2023, 8, 8); * const gloc = new GeoLocation(null, latitude, longitude, 0, tzid); * const zmanim = new Zmanim(gloc, friday, false); * const candleLighting = zmanim.sunsetOffset(-18, true); * const timeStr = Zmanim.formatISOWithTimeZone(tzid, candleLighting); */ class Zmanim { /** * Initialize a Zmanim instance. * @param gloc GeoLocation including latitude, longitude, and timezone * @param date Regular or Hebrew Date. If `date` is a regular `Date`, * hours, minutes, seconds and milliseconds are ignored. * @param useElevation use elevation for calculations (default `false`). * If `true`, use elevation to affect the calculation of all sunrise/sunset based * zmanim. Note: there are some zmanim such as degree-based zmanim that are driven * by the amount of light in the sky and are not impacted by elevation. * These zmanim intentionally do not support elevation adjustment. */ constructor(gloc, date, useElevation) { const dt = getDate(date); this.date = dt; this.gloc = gloc; const plainDate = Temporal.PlainDate.from({ year: dt.getFullYear(), month: dt.getMonth() + 1, day: dt.getDate(), }); this.noaa = new NOAACalculator(gloc, plainDate); this.useElevation = Boolean(useElevation); } /** * Returns `true` if elevation adjustment is enabled * for zmanim support elevation adjustment */ getUseElevation() { return this.useElevation; } /** * Enables or disables elevation adjustment for zmanim support elevation adjustment * @param useElevation */ setUseElevation(useElevation) { this.useElevation = useElevation; } /** * Convenience function to get the time when sun is above or below the horizon * for a certain angle (in degrees). * This function does not support elevation adjustment. * @param angle * @param rising */ timeAtAngle(angle, rising) { const offsetZenith = 90 + angle; const zdt = rising ? this.noaa.getSunriseOffsetByDegrees(offsetZenith) : this.noaa.getSunsetOffsetByDegrees(offsetZenith); return zdtToDate(zdt); } /** * Upper edge of the Sun appears over the eastern horizon in the morning (0.833° above horizon) * If elevation is enabled, this function will include elevation in the calculation. */ sunrise() { const zdt = this.useElevation ? this.noaa.getSunrise() : this.noaa.getSeaLevelSunrise(); return zdtToDate(zdt); } /** * Upper edge of the Sun appears over the eastern horizon in the morning (0.833° above horizon). * This function does not support elevation adjustment. */ seaLevelSunrise() { const zdt = this.noaa.getSeaLevelSunrise(); return zdtToDate(zdt); } /** * When the upper edge of the Sun disappears below the horizon (0.833° below horizon). * If elevation is enabled, this function will include elevation in the calculation. */ sunset() { const zdt = this.useElevation ? this.noaa.getSunset() : this.noaa.getSeaLevelSunset(); return zdtToDate(zdt); } /** * When the upper edge of the Sun disappears below the horizon (0.833° below horizon). * This function does not support elevation adjustment. */ seaLevelSunset() { const zdt = this.noaa.getSeaLevelSunset(); return zdtToDate(zdt); } /** * Civil dawn; Sun is 6° below the horizon in the morning. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ dawn() { const zdt = this.noaa.getBeginCivilTwilight(); return zdtToDate(zdt); } /** * Civil dusk; Sun is 6° below the horizon in the evening. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ dusk() { const zdt = this.noaa.getEndCivilTwilight(); return zdtToDate(zdt); } /** * Returns sunset for the previous day. * If elevation is enabled, this function will include elevation in the calculation. */ gregEve() { const prev = new Date(this.date); prev.setDate(prev.getDate() - 1); const zman = new Zmanim(this.gloc, prev, this.useElevation); return zman.sunset(); } /** * @private */ nightHour() { return (this.sunrise().getTime() - this.gregEve().getTime()) / 12; // ms in hour } /** * Midday – Chatzot; Sunrise plus 6 halachic hours */ chatzot() { const startOfDay = this.noaa.getSeaLevelSunrise(); const endOfDay = this.noaa.getSeaLevelSunset(); const zdt = this.noaa.getSunTransit(startOfDay, endOfDay); return zdtToDate(zdt); } /** * Midnight – Chatzot; Sunset plus 6 halachic hours. * If elevation is enabled, this function will include elevation in the calculation. */ chatzotNight() { return new Date(this.sunrise().getTime() - this.nightHour() * 6); } /** * Dawn – Alot haShachar; Sun is 16.1° below the horizon in the morning. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ alotHaShachar() { return this.timeAtAngle(16.1, true); } /** * Dawn – Alot haShachar; calculated as 72 minutes before sunrise or * sea level sunrise. */ alotHaShachar72() { return this.sunriseOffset(-72, false, false); } /** * Earliest talis & tefillin – Misheyakir; Sun is 11.5° below the horizon in the morning. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ misheyakir() { return this.timeAtAngle(11.5, true); } /** * Earliest talis & tefillin – Misheyakir Machmir; Sun is 10.2° below the horizon in the morning. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ misheyakirMachmir() { return this.timeAtAngle(10.2, true); } /** * Utility method for using elevation-aware sunrise/sunset * @private * @param hours */ getShaahZmanisBasedZman(hours) { const startOfDay = this.useElevation ? this.noaa.getSunrise() : this.noaa.getSeaLevelSunrise(); const endOfDay = this.useElevation ? this.noaa.getSunset() : this.noaa.getSeaLevelSunset(); const temporalHour = this.noaa.getTemporalHour(startOfDay, endOfDay); const offset = Math.round(temporalHour * hours); const zdt = NOAACalculator.getTimeOffset(startOfDay, offset); return zdtToDate(zdt); } /** * Latest Shema (Gra); Sunrise plus 3 halachic hours, according to the Gra. * If elevation is enabled, this function will include elevation in the calculation. */ sofZmanShma() { // Gra return this.getShaahZmanisBasedZman(3); } /** * Latest Shacharit (Gra); Sunrise plus 4 halachic hours, according to the Gra. * * This method returns the latest *zman tfila* (time to recite shema in the morning) * that is 4 *shaos zmaniyos* (solar hours) after sunrise or sea level sunrise * (depending on the `useElevation` setting), according * to the [GRA](https://en.wikipedia.org/wiki/Vilna_Gaon). * * If elevation is enabled, this function will include elevation in the calculation. */ sofZmanTfilla() { // Gra return this.getShaahZmanisBasedZman(4); } /** * Returns an array with alot (Date) and ms in hour (number) * @private */ getTemporalHour72(forceSeaLevel) { const alot72 = this.sunriseOffset(-72, false, forceSeaLevel); const tzeit72 = this.sunsetOffset(72, false, forceSeaLevel); const temporalHour = (tzeit72.getTime() - alot72.getTime()) / 12; return [alot72, temporalHour]; } /** * Returns an array with alot (Date) and ms in hour (number) * @private */ getTemporalHourByDeg(angle) { const alot = this.timeAtAngle(angle, true); const tzeit = this.timeAtAngle(angle, false); const temporalHour = (tzeit.getTime() - alot.getTime()) / 12; return [alot, temporalHour]; } /** * Latest Shema (MGA); Sunrise plus 3 halachic hours, according to Magen Avraham. * Based on the opinion of the MGA that the day is calculated from * dawn being fixed 72 minutes before sea-level sunrise, and nightfall is fixed * 72 minutes after sea-level sunset. */ sofZmanShmaMGA() { // Magen Avraham const [alot72, temporalHour] = this.getTemporalHour72(true); const offset = Math.floor(3 * temporalHour); return new Date(alot72.getTime() + offset); } /** * Latest Shema (MGA); Sunrise plus 3 halachic hours, according to Magen Avraham. * Based on the opinion of the MGA that the day is calculated from * dawn to nightfall with both being 16.1° below the horizon. */ sofZmanShmaMGA16Point1() { const [alot, temporalHour] = this.getTemporalHourByDeg(16.1); const offset = Math.floor(3 * temporalHour); return new Date(alot.getTime() + offset); } /** * Latest Shema (MGA); Sunrise plus 3 halachic hours, according to Magen Avraham. * Based on the opinion of the MGA that the day is calculated from * dawn to nightfall with both being 19.8° below the horizon. * * This calculation is based on the position of the sun 90 minutes after sunset in Jerusalem * around the equinox / equilux which calculates to 19.8° below geometric zenith. * https://kosherjava.com/2022/01/12/equinox-vs-equilux-zmanim-calculations/ */ sofZmanShmaMGA19Point8() { const [alot, temporalHour] = this.getTemporalHourByDeg(19.8); const offset = Math.floor(3 * temporalHour); return new Date(alot.getTime() + offset); } /** * Latest Shacharit (MGA); Sunrise plus 4 halachic hours, according to Magen Avraham */ sofZmanTfillaMGA() { // Magen Avraham const [alot72, temporalHour] = this.getTemporalHour72(true); const offset = Math.floor(4 * temporalHour); return new Date(alot72.getTime() + offset); } /** * Latest Shacharit (MGA); Sunrise plus 4 halachic hours, according to Magen Avraham. * Based on the opinion of the MGA that the day is calculated from * dawn to nightfall with both being 16.1° below the horizon. */ sofZmanTfillaMGA16Point1() { const [alot, temporalHour] = this.getTemporalHourByDeg(16.1); const offset = Math.floor(4 * temporalHour); return new Date(alot.getTime() + offset); } /** * Latest Shacharit (MGA); Sunrise plus 4 halachic hours, according to Magen Avraham. * Based on the opinion of the MGA that the day is calculated from * dawn to nightfall with both being 19.8° below the horizon. * * This calculation is based on the position of the sun 90 minutes after sunset in Jerusalem * around the equinox / equilux which calculates to 19.8° below geometric zenith. * https://kosherjava.com/2022/01/12/equinox-vs-equilux-zmanim-calculations/ */ sofZmanTfillaMGA19Point8() { const [alot, temporalHour] = this.getTemporalHourByDeg(19.8); const offset = Math.floor(4 * temporalHour); return new Date(alot.getTime() + offset); } /** * Earliest Mincha – Mincha Gedola (GRA); Sunrise plus 6.5 halachic hours. * If elevation is enabled, this function will include elevation in the calculation. * * This method returns the latest mincha gedola, the earliest time one can pray mincha * that is 6.5 shaos zmaniyos (solar hours) after sunrise or sea level sunrise * (depending on the `useElevation` setting), according * to the [GRA](https://en.wikipedia.org/wiki/Vilna_Gaon). * * The Ramba"m is of the opinion that it is better to delay *mincha* until * *mincha ketana* while the Ra"sh, Tur, GRA and others are of the * opinion that *mincha* can be prayed *lechatchila* starting at *mincha gedola*. */ minchaGedola() { return this.getShaahZmanisBasedZman(6.5); } /** * Earliest Mincha – Mincha Gedola (MGA); Sunrise plus 6.5 halachic hours. * If elevation is enabled, this function will include elevation in the calculation. * * This method returns the time of *mincha gedola* according to the Magen Avraham * with the day starting 72 minutes before sunrise and ending 72 minutes after sunset. * This is the earliest time to pray *mincha*. */ minchaGedolaMGA() { const [alot72, temporalHour] = this.getTemporalHour72(false); const offset = Math.floor(6.5 * temporalHour); return new Date(alot72.getTime() + offset); } /** * Preferable earliest time to recite Minchah – Mincha Ketana; Sunrise plus 9.5 halachic hours. * If elevation is enabled, this function will include elevation in the calculation. * * This method returns *mincha ketana*, the preferred earliest time to pray *mincha* in the * opinion of the [Rambam](https://en.wikipedia.org/wiki/Maimonides) and others, * that is 9.5 *shaos zmaniyos* (solar hours) after sunrise or sea level sunrise * (depending on the `useElevation` setting), according * to the [GRA](https://en.wikipedia.org/wiki/Vilna_Gaon). */ minchaKetana() { return this.getShaahZmanisBasedZman(9.5); } /** * This method returns the time of *mincha ketana* according to the Magen Avraham * with the day starting 72 minutes before sunrise and ending 72 minutes after sunset. * This is the preferred earliest time to pray *mincha* according to the opinion of * the [Rambam](https://en.wikipedia.org/wiki/Maimonides) and others. * * If elevation is enabled, this function will include elevation in the calculation. */ minchaKetanaMGA() { const [alot72, temporalHour] = this.getTemporalHour72(false); return new Date(alot72.getTime() + Math.floor(9.5 * temporalHour)); } /** * Plag haMincha; Sunrise plus 10.75 halachic hours. * If elevation is enabled, this function will include elevation in the calculation. */ plagHaMincha() { return this.getShaahZmanisBasedZman(10.75); } /** * @param [angle=8.5] optional time for solar depression. * Default is 8.5 degrees for 3 small stars, use 7.083 degrees for 3 medium-sized stars. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ tzeit(angle = 8.5) { return this.timeAtAngle(angle, false); } /** * Alias for sunrise */ neitzHaChama() { return this.sunrise(); } /** * Alias for sunset */ shkiah() { return this.sunset(); } /** * Rabbeinu Tam holds that bein hashmashos is a specific time * between sunset and tzeis hakochavim. * One opinion on how to calculate this time is that * it is 13.5 minutes before tzies 7.083. * Because degree-based functions estimate the amount of light in the sky, * the result is not impacted by elevation. */ beinHaShmashos() { const tzeit = this.tzeit(7.083); const millis = tzeit.getTime(); if (isNaN(millis)) { return tzeit; } return new Date(millis - 13.5 * 60 * 1000); } /** * Uses timeFormat to return a date like '20:34'. * Returns `XX:XX` if the date is invalid. */ static formatTime(dt, timeFormat) { if (isNaN(dt.getTime())) { return 'XX:XX'; // Invalid Date } const time = timeFormat.format(dt); const hm = time.split(':'); if (hm[0] === '24') { return '00:' + hm[1]; } return time; } /** * Discards seconds, rounding to nearest minute. * @param dt */ static roundTime(dt) { const millis = dt.getTime(); if (isNaN(millis)) { return dt; } // Round up to next minute if needed const millisOnly = dt.getMilliseconds(); const seconds = dt.getSeconds(); if (seconds === 0 && millisOnly === 0) { return dt; } const secAndMillis = seconds * 1000 + millisOnly; const delta = secAndMillis >= 30000 ? 60000 - secAndMillis : -1 * secAndMillis; return new Date(millis + delta); } /** * Get offset string (like "+05:00" or "-08:00") from tzid (like "Europe/Moscow") * @param tzid * @param date */ static timeZoneOffset(tzid, date) { const offset = getTimezoneOffset(tzid, date); const offsetAbs = Math.abs(offset); const hours = Math.floor(offsetAbs / 60); const minutes = offsetAbs % 60; return (offset < 0 ? '+' : '-') + pad2(hours) + ':' + pad2(minutes); } /** * Returns a string like "2022-04-01T13:06:00-11:00" * @param tzid * @param date */ static formatISOWithTimeZone(tzid, date) { if (isNaN(date.getTime())) { return '0000-00-00T00:00:00Z'; } return (getPseudoISO(tzid, date).substring(0, 19) + Zmanim.timeZoneOffset(tzid, date)); } /** * Returns sunrise + `offset` minutes (either positive or negative). * If elevation is enabled, this function will include elevation in the calculation * unless `forceSeaLevel` is `true`. * @param offset minutes * @param roundMinute round time to nearest minute (default true) * @param forceSeaLevel use sea-level sunrise (default false) */ sunriseOffset(offset, roundMinute = true, forceSeaLevel = false) { const sunrise = forceSeaLevel ? this.seaLevelSunrise() : this.sunrise(); if (isNaN(sunrise.getTime())) { return sunrise; } if (roundMinute) { // For positive offsets only, round up to next minute if needed if (offset > 0 && sunrise.getSeconds() >= 30) { offset++; } sunrise.setSeconds(0, 0); } return new Date(sunrise.getTime() + offset * 60 * 1000); } /** * Returns sunset + `offset` minutes (either positive or negative). * If elevation is enabled, this function will include elevation in the calculation * unless `forceSeaLevel` is `true`. * @param offset minutes * @param roundMinute round time to nearest minute (default true) * @param forceSeaLevel use sea-level sunset (default false) */ sunsetOffset(offset, roundMinute = true, forceSeaLevel = false) { const sunset = forceSeaLevel ? this.seaLevelSunset() : this.sunset(); if (isNaN(sunset.getTime())) { return sunset; } if (roundMinute) { // For Havdalah only, round up to next minute if needed if (offset > 0 && sunset.getSeconds() >= 30) { offset++; } sunset.setSeconds(0, 0); } return new Date(sunset.getTime() + offset * 60 * 1000); } /** * Returns the Hebrew date relative to the specified location and Gregorian date, * taking into consideration whether the time is before or after sunset. * * For example, if the given date and is `2024-09-22T10:35` (before sunset), and * sunset for the specified location is **19:04**, then this function would * return a Hebrew date of `19th of Elul, 5784`. * If the given date is the same Gregorian day after sunset * (for example `2024-09-22T20:07`), this function would return a * Hebrew date of `20th of Elul, 5784`. * @example * const {GeoLocation, Zmanim, HDate} = require('@hebcal/core'); * const latitude = 48.85341; * const longitude = 2.3488; * const timezone = 'Europe/Paris'; * const gloc = new GeoLocation(null, latitude, longitude, 0, timezone); * const before = Zmanim.makeSunsetAwareHDate(gloc, new Date('2024-09-22T17:38:46.123Z'), false); * console.log(before.toString()); // '19 Elul 5784' * const after = Zmanim.makeSunsetAwareHDate(gloc, new Date('2024-09-22T23:45:18.345Z'), false); * console.log(after.toString()); // '20 Elul 5784' */ static makeSunsetAwareHDate(gloc, date, useElevation) { const zmanim = new Zmanim(gloc, date, useElevation); const sunset = zmanim.sunset(); let hd = new HDate(date); const sunsetMillis = sunset.getTime(); if (isNaN(sunsetMillis)) { return hd; } if (date.getTime() >= sunsetMillis) { hd = hd.next(); } return hd; } } export { Zmanim }; //# sourceMappingURL=zmanim.js.map