Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python package

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:


jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.7","3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python setup.py install
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
8 changes: 8 additions & 0 deletions test/test_astronomical_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ def test_entry(geo):

for entry in expected:
self.assertEqual(test_entry(entry[0]), entry)

def test_temporal_hour_on_day_without_sunset_or_without_sunrise(self):
calc = AstronomicalCalendar(date=date(2023, 6, 20)) # Middle of the North Pole summer
calc.geo_location = test_helper.daneborg()
self.assertIsNone(calc.temporal_hour())
calc = AstronomicalCalendar(date=date(2023, 1, 20)) # Middle of the North Pole winter
calc.geo_location = test_helper.daneborg()
self.assertIsNone(calc.temporal_hour())


if __name__ == '__main__':
Expand Down
31 changes: 31 additions & 0 deletions test/test_zmanim_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,37 @@ def test_assur_bemelacha_on_first_of_two_issur_melacha_days_in_israel(self):
calendar = ZmanimCalendar(geo_location=test_helper.lakewood(), date=parser.parse(date))
self.assertTrue(calendar.is_assur_bemelacha(calendar.tzais() - timedelta(seconds=2), in_israel=True))
self.assertTrue(calendar.is_assur_bemelacha(calendar.tzais() + timedelta(seconds=2), in_israel=True))

def test_sof_zman_shma_gra_on_day_without_sunset_or_without_sunrise(self):
date = '2023-06-20' # Middle of the North Pole summer
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=parser.parse(date))
self.assertEqual(calendar.sof_zman_shma_gra(), None)

date = '2023-01-20' # Middle of the North Pole Winter
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=parser.parse(date))
self.assertEqual(calendar.sof_zman_shma_gra(), None)

def test_sof_zman_shma_mga_on_day_without_sunset_or_without_sunrise(self):
date = '2023-06-20' # Middle of the North Pole summer
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=parser.parse(date))
self.assertEqual(calendar.sof_zman_shma_mga(), None)

date = '2023-01-20' # Middle of the North Pole Winter
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=parser.parse(date))
self.assertEqual(calendar.sof_zman_shma_mga(), None)

def test__offset_by_minutes_zmanis_on_day_without_sunset_or_without_sunrise(self):
date = '2023-06-20' # Middle of the North Pole summer
date_object = parser.parse(date)
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=date_object)
self.assertEqual(calendar._offset_by_minutes_zmanis(date_object,10), None)

date = '2023-01-20' # Middle of the North Pole Winter
date_object = parser.parse(date)
calendar = ZmanimCalendar(geo_location=test_helper.daneborg(), date=date_object)
self.assertEqual(calendar._offset_by_minutes_zmanis(date_object,10), None)




if __name__ == '__main__':
Expand Down
7 changes: 5 additions & 2 deletions zmanim/astronomical_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def utc_sunset(self, zenith: float) -> Optional[float]:
def utc_sea_level_sunset(self, zenith: float) -> Optional[float]:
return self.astronomical_calculator.utc_sunset(self._adjusted_date(), self.geo_location, zenith, adjust_for_elevation=False)

def temporal_hour(self, sunrise: Optional[datetime] = __sentinel, sunset: Optional[datetime] = __sentinel) -> Optional[float]:
def temporal_hour(self, sunrise: Optional[datetime] = __sentinel, sunset: Optional[datetime] = __sentinel) -> Optional[float]: # type: ignore
if sunrise == self.__sentinel:
sunrise = self.sea_level_sunrise()
if sunset == self.__sentinel:
Expand All @@ -79,7 +79,10 @@ def sun_transit(self) -> Optional[datetime]:
sunset = self.sea_level_sunset()
if sunrise is None or sunset is None:
return None
noon_hour = (self.temporal_hour(sunrise, sunset) / self.HOUR_MILLIS) * 6.0
temporal_hour = self.temporal_hour(sunrise, sunset)
if temporal_hour is None:
return None
noon_hour = (temporal_hour / self.HOUR_MILLIS) * 6.0
return sunrise + timedelta(noon_hour / 24.0)

def _date_time_from_time_of_day(self, time_of_day: Optional[float], mode: str) -> Optional[datetime]:
Expand Down
35 changes: 18 additions & 17 deletions zmanim/hebrew_calendar/jewish_date.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import copy
from datetime import date, timedelta
from datetime import timedelta
from datetime import date as dt_date
from enum import Enum
from memoization import cached
from typing import Optional
from typing import Optional, Tuple


class JewishDate:
MONTHS = Enum('Months', 'nissan iyar sivan tammuz av elul tishrei cheshvan kislev teves shevat adar adar_ii')
MONTHS_LIST = list(MONTHS)

RD = date(1, 1, 1)
RD = dt_date(1, 1, 1)
JEWISH_EPOCH = -1373429

CHALAKIM_PER_MINUTE = 18
Expand All @@ -26,7 +27,7 @@ def __init__(self, *args, **kwargs):
self.reset_date()
elif len(args) == 3:
self.set_jewish_date(*args, **kwargs)
elif len(args) == 1 and isinstance(args[0], date):
elif len(args) == 1 and isinstance(args[0], dt_date):
self.date = args[0]
elif len(args) == 1 and isinstance(args[0], int):
self._set_from_molad(*args)
Expand All @@ -39,7 +40,7 @@ def __repr__(self):
self.jewish_date, self.day_of_week, self.molad_hours, self.molad_minutes, self.molad_chalakim)

@property
def gregorian_date(self) -> date:
def gregorian_date(self) -> dt_date:
return self.__gregorian_date

@property
Expand Down Expand Up @@ -71,7 +72,7 @@ def day_of_week(self) -> int:
return self.__day_of_week

@property
def jewish_date(self) -> (int, int, int):
def jewish_date(self) -> Tuple[int, int, int]:
return self.__jewish_year, self.__jewish_month, self.__jewish_day

@property
Expand Down Expand Up @@ -131,11 +132,11 @@ def from_jewish_date(cls, year: int, month: int, date: int) -> 'JewishDate':
return cls(year, month, date)

@classmethod
def from_date(cls, date: date) -> 'JewishDate':
def from_date(cls, date: dt_date) -> 'JewishDate':
return cls(date)

def reset_date(self) -> 'JewishDate':
self.date = date.today()
self.date = dt_date.today()
return self

def set_jewish_date(self, year: int, month: int, day: int, hours: int = 0, minutes: int = 0, chalakim: int = 0):
Expand All @@ -159,7 +160,7 @@ def set_gregorian_date(self, year: int, month: int, day: int):
raise ValueError("invalid date parts")
max_days = self.days_in_gregorian_month(month, year)
day = max_days if day > max_days else day
self.date = date(year, month, day)
self.date = dt_date(year, month, day)

def forward(self, increment: int = 1) -> 'JewishDate':
if increment < 0:
Expand Down Expand Up @@ -237,7 +238,7 @@ def __sub__(self, subtrahend):
return type(self)(self.gregorian_date - subtrahend)
elif isinstance(subtrahend, JewishDate):
return self.gregorian_date - subtrahend.gregorian_date
elif isinstance(subtrahend, date):
elif isinstance(subtrahend, dt_date):
return self.gregorian_date - subtrahend
raise ValueError

Expand Down Expand Up @@ -366,7 +367,7 @@ def is_jewish_leap_year(self, year: Optional[int] = None) -> bool:
year = self.jewish_year
return self._is_jewish_leap_year(year)

def cheshvan_kislev_kviah(self, year: Optional[int] = None) -> str:
def cheshvan_kislev_kviah(self, year: Optional[int] = None) -> CHESHVAN_KISLEV_KEVIAH:
if year is None:
year = self.jewish_year
year_type = (self.days_in_jewish_year(year) % 10) - 3
Expand All @@ -382,7 +383,7 @@ def kviah(self, year: Optional[int] = None) -> tuple:
pesach_day = date.day_of_week
return rosh_hashana_day, kviah_value, pesach_day

def molad(self, month: int = None, year: Optional[int] = None) -> 'JewishDate':
def molad(self, month: Optional[int] = None, year: Optional[int] = None) -> 'JewishDate':
if month is None:
month = self.jewish_month
if year is None:
Expand Down Expand Up @@ -419,7 +420,7 @@ def _jewish_date_to_abs_date(self, year: int, month: int, day: int) -> int:
return self.day_number_of_jewish_year(year, month, day) + \
self._jewish_year_start_to_abs_date(year) - 1

def _jewish_date_from_abs_date(self, absolute_date: int) -> (int, int, int):
def _jewish_date_from_abs_date(self, absolute_date: int) -> Tuple[int, int, int]:
jewish_year = int((absolute_date - self.JEWISH_EPOCH) / 366)

# estimate may be low for CE
Expand All @@ -437,11 +438,11 @@ def _jewish_date_from_abs_date(self, absolute_date: int) -> (int, int, int):

return jewish_year, jewish_month, jewish_day

def _gregorian_date_to_abs_date(self, gregorian_date: date) -> int:
def _gregorian_date_to_abs_date(self, gregorian_date: dt_date) -> int:
return gregorian_date.toordinal()

def _gregorian_date_from_abs_date(self, absolute_date: int) -> date:
return date.fromordinal(absolute_date)
def _gregorian_date_from_abs_date(self, absolute_date: int) -> dt_date:
return dt_date.fromordinal(absolute_date)

def _molad_to_abs_date(self, chalakim: int) -> int:
return int(chalakim / self.CHALAKIM_PER_DAY) + self.JEWISH_EPOCH
Expand Down Expand Up @@ -516,7 +517,7 @@ def _dechiyos_count(year: int, days: int, remainder: int) -> int:
return count

@staticmethod
def _molad_components_for_year(year: int) -> (int, int):
def _molad_components_for_year(year: int) -> Tuple[int, int]:
chalakim = JewishDate._chalakim_since_molad_tohu(year, 7) # chalakim up to tishrei of given year
days, remainder = divmod(chalakim, JewishDate.CHALAKIM_PER_DAY)
return int(days), int(remainder)
Expand Down
18 changes: 16 additions & 2 deletions zmanim/util/astronomical_calculations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import math
from datetime import date
from typing import Optional
from abc import ABC, abstractmethod

from zmanim.util.geo_location import GeoLocation

class AstronomicalCalculations:


class AstronomicalCalculations(ABC):
GEOMETRIC_ZENITH = 90.0

def __init__(self):
Expand All @@ -15,4 +21,12 @@ def elevation_adjustment(self, elevation: float) -> float:
def adjusted_zenith(self, zenith: float, elevation: float) -> float:
if zenith != self.GEOMETRIC_ZENITH:
return zenith
return zenith + self.solar_radius + self.refraction + self.elevation_adjustment(elevation)
return zenith + self.solar_radius + self.refraction + self.elevation_adjustment(elevation)

@abstractmethod
def utc_sunrise(self, target_date: date, geo_location: GeoLocation, zenith: float, adjust_for_elevation: bool = False) -> Optional[float]:
pass

@abstractmethod
def utc_sunset(self, target_date: date, geo_location: GeoLocation, zenith: float, adjust_for_elevation: bool = False) -> Optional[float]:
pass
22 changes: 16 additions & 6 deletions zmanim/util/geo_location.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional
from datetime import datetime, tzinfo
from typing import Optional, Union

from dateutil import tz

Expand Down Expand Up @@ -62,13 +62,16 @@ def longitude(self, longitude):
raise TypeError("input must be a number or a list in the format 'degrees,minutes,seconds,direction'")

@property
def time_zone(self) -> tz.tzfile:
def time_zone(self) -> Union[tz.tzfile,tzinfo]:
return self.__time_zone

@time_zone.setter
def time_zone(self, time_zone):
if isinstance(time_zone, str):
self.__time_zone = tz.gettz(time_zone)
time_zone = tz.gettz(time_zone)
if time_zone is None:
raise ValueError("invalid time zone")
self.__time_zone = time_zone
elif isinstance(time_zone, tz.tzfile):
self.__time_zone = time_zone
else:
Expand Down Expand Up @@ -109,7 +112,14 @@ def local_mean_time_offset(self) -> float:

def standard_time_offset(self) -> int:
now = datetime.now(tz=self.time_zone)
return int((now.utcoffset() - now.dst()).total_seconds()) * 1000
utcoffset = now.utcoffset()
dst = now.dst()
if utcoffset is None or dst is None:
raise ValueError("Could not determine time zone offset or DST")
return int((utcoffset - dst).total_seconds()) * 1000

def time_zone_offset_at(self, utc_time: datetime) -> float:
return utc_time.astimezone(self.time_zone).utcoffset().total_seconds() / 3600.0
utcoffset = utc_time.astimezone(self.time_zone).utcoffset()
if utcoffset is None:
raise ValueError("Could not determine time zone offset")
return utcoffset.total_seconds() / 3600.0
37 changes: 28 additions & 9 deletions zmanim/zmanim_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class ZmanimCalendar(AstronomicalCalendar):
def __init__(self, candle_lighting_offset: int = None, *args, **kwargs):
def __init__(self, candle_lighting_offset: Optional[int] = None, *args, **kwargs):
super(ZmanimCalendar, self).__init__(*args, **kwargs)
self.candle_lighting_offset = 18 if candle_lighting_offset is None else candle_lighting_offset
self.use_elevation = False
Expand Down Expand Up @@ -56,14 +56,22 @@ def chatzos(self) -> Optional[datetime]:
def candle_lighting(self) -> Optional[datetime]:
return self._offset_by_minutes(self.sea_level_sunset(), -self.candle_lighting_offset)

def sof_zman_shma(self, day_start: datetime, day_end: datetime) -> datetime:
def sof_zman_shma(self, day_start: datetime, day_end: datetime) -> Optional[datetime]:
return self._shaos_into_day(day_start, day_end, 3)

def sof_zman_shma_gra(self) -> datetime:
return self.sof_zman_shma(self.elevation_adjusted_sunrise(), self.elevation_adjusted_sunset())
def sof_zman_shma_gra(self) -> Optional[datetime]:
elevation_adjusted_sunrise = self.elevation_adjusted_sunrise()
elevation_adjusted_sunset = self.elevation_adjusted_sunset()
if elevation_adjusted_sunrise is None or elevation_adjusted_sunset is None:
return None
return self.sof_zman_shma(elevation_adjusted_sunrise, elevation_adjusted_sunset)

def sof_zman_shma_mga(self) -> datetime:
return self.sof_zman_shma(self.alos_72(), self.tzais_72())
def sof_zman_shma_mga(self) -> Optional[datetime]:
alos_72 = self.alos_72()
tzais_72 = self.tzais_72()
if alos_72 is None or tzais_72 is None:
return None
return self.sof_zman_shma(alos_72, tzais_72)

def sof_zman_tfila(self, day_start: Optional[datetime], day_end: Optional[datetime]) -> Optional[datetime]:
return self._shaos_into_day(day_start, day_end, 4)
Expand Down Expand Up @@ -111,17 +119,25 @@ def shaah_zmanis_by_degrees_and_offset(self, degrees: float, offset: float) -> O
opts = {'degrees': degrees, 'offset': offset}
return self.shaah_zmanis(self.alos(opts), self.tzais(opts))

def is_assur_bemelacha(self, current_time: datetime, tzais=None, in_israel: Optional[bool]=False):
def is_assur_bemelacha(self, current_time: datetime, tzais=None, in_israel: Optional[bool]=False) -> Optional[bool]:
if tzais is None:
tzais_time = self.tzais()
elif isinstance(tzais, dict):
tzais_time = self.tzais(tzais)
else:
tzais_time = tzais

if tzais_time is None:
return None

elevation_adjusted_sunset = self.elevation_adjusted_sunset()
if elevation_adjusted_sunset is None:
return None

jewish_calendar = JewishCalendar(current_time.date())
jewish_calendar.in_israel = in_israel
return (current_time <= tzais_time and jewish_calendar.is_assur_bemelacha()) or \
(current_time >= self.elevation_adjusted_sunset() and jewish_calendar.is_tomorrow_assur_bemelacha())
(current_time >= elevation_adjusted_sunset and jewish_calendar.is_tomorrow_assur_bemelacha())

def _shaos_into_day(self, day_start: Optional[datetime], day_end: Optional[datetime], shaos: float) -> Optional[datetime]:
shaah_zmanis = self.temporal_hour(day_start, day_end)
Expand All @@ -143,5 +159,8 @@ def _offset_by_minutes(self, time: Optional[datetime], minutes: float) -> Option
def _offset_by_minutes_zmanis(self, time: Optional[datetime], minutes: float) -> Optional[datetime]:
if time is None:
return None
shaah_zmanis_skew = self.shaah_zmanis_gra() / self.HOUR_MILLIS
shaah_zmanis_gra = self.shaah_zmanis_gra()
if shaah_zmanis_gra is None:
return None
shaah_zmanis_skew = shaah_zmanis_gra / self.HOUR_MILLIS
return time + timedelta(minutes=minutes*shaah_zmanis_skew)