"""The weekly Torah reading (parashat hashavua).
The annual cycle begins with Bereshit on the first Shabbat after Simchat Torah and
runs through Ha'azinu the following autumn (Vezot Haberachah is read on Simchat
Torah itself, not on a Shabbat). Which of the seven combinable pairs are read
together is fixed by the year type (leap, Rosh Hashanah weekday, year length, and
Israel vs Diaspora); the table below was verified against an independent reference
on thousands of Shabbatot. Shabbatot coinciding with a festival have no weekly
parasha (a festival reading is used instead) and are returned as ``None``.
"""
from __future__ import annotations
from hebrewcal.calendars.hebrew import HebrewDate
from hebrewcal.hebrew.metonic import is_leap_year
from hebrewcal.hebrew.yeartype import days_in_year, new_year_rd
PARSHIYOT = (
"Bereshit", "Noach", "Lech-Lecha", "Vayera", "Chayei Sarah", "Toldot",
"Vayetzei", "Vayishlach", "Vayeshev", "Miketz", "Vayigash", "Vayechi",
"Shemot", "Va'era", "Bo", "Beshalach", "Yitro", "Mishpatim", "Terumah",
"Tetzaveh", "Ki Tisa", "Vayakhel", "Pekudei", "Vayikra", "Tzav", "Shemini",
"Tazria", "Metzora", "Acharei Mot", "Kedoshim", "Emor", "Behar", "Bechukotai",
"Bamidbar", "Nasso", "Beha'alotcha", "Shelach", "Korach", "Chukat", "Balak",
"Pinchas", "Matot", "Masei", "Devarim", "Va'etchanan", "Eikev", "Re'eh",
"Shoftim", "Ki Teitzei", "Ki Tavo", "Nitzavim", "Vayelech", "Ha'azinu",
"Vezot Haberachah",
)
# Adjacent combinable pairs, by the first parashah's index.
_PAIRS = {21, 26, 28, 31, 38, 41, 50}
# Year type -> the set of pair-first-indices read combined. Key:
# (is_leap, rosh_hashanah_weekday, year_length, israel). Verified against an
# independent reference over 5760-5840 (Israel and Diaspora), zero mismatches.
_COMBOS: dict[tuple[bool, int, int, bool], frozenset[int]] = {
(False, 1, 353, False): frozenset({21, 26, 28, 31, 41, 50}),
(False, 1, 355, False): frozenset({21, 26, 28, 31, 38, 41, 50}),
(False, 2, 354, False): frozenset({21, 26, 28, 31, 38, 41, 50}),
(False, 4, 354, False): frozenset({21, 26, 28, 31, 41}),
(False, 4, 355, False): frozenset({26, 28, 31, 41}),
(False, 6, 353, False): frozenset({21, 26, 28, 31, 41}),
(False, 6, 355, False): frozenset({21, 26, 28, 31, 41, 50}),
(True, 1, 383, False): frozenset({38, 41, 50}),
(True, 1, 385, False): frozenset({41}),
(True, 2, 384, False): frozenset({41}),
(True, 4, 383, False): frozenset(),
(True, 4, 385, False): frozenset({50}),
(True, 6, 383, False): frozenset({41, 50}),
(True, 6, 385, False): frozenset({38, 41, 50}),
(False, 1, 353, True): frozenset({21, 26, 28, 31, 41, 50}),
(False, 1, 355, True): frozenset({21, 26, 28, 31, 41, 50}),
(False, 2, 354, True): frozenset({21, 26, 28, 31, 41, 50}),
(False, 4, 354, True): frozenset({21, 26, 28, 41}),
(False, 4, 355, True): frozenset({26, 28, 31, 41}),
(False, 6, 353, True): frozenset({21, 26, 28, 31, 41}),
(False, 6, 355, True): frozenset({21, 26, 28, 31, 41, 50}),
(True, 1, 383, True): frozenset({41, 50}),
(True, 1, 385, True): frozenset(),
(True, 2, 384, True): frozenset(),
(True, 4, 383, True): frozenset(),
(True, 4, 385, True): frozenset({50}),
(True, 6, 383, True): frozenset({41, 50}),
(True, 6, 385, True): frozenset({41, 50}),
}
def _displaced(israel: bool) -> set[tuple[int, int]]:
"""Return the (month, day) pairs whose Shabbat carries a festival reading."""
days: set[tuple[int, int]] = {(7, 1), (7, 2), (7, 10)}
days |= {(7, d) for d in range(15, 23)} # Sukkot through Shemini Atzeret
if not israel:
days.add((7, 23)) # Simchat Torah (Diaspora)
days |= {(1, d) for d in range(15, 22)} # Pesach
if not israel:
days.add((1, 22))
days.add((3, 6)) # Shavuot
if not israel:
days.add((3, 7))
return days
def _year_combos(year: int, israel: bool) -> frozenset[int]:
key = (is_leap_year(year), new_year_rd(year) % 7, days_in_year(year), israel)
return _COMBOS[key]
def _readings(year: int, israel: bool) -> list[str]:
"""Return the ordered Shabbat readings for the cycle whose spring is in ``year``."""
combos = _year_combos(year, israel)
out: list[str] = []
i = 0
while i <= 52: # Bereshit (0) through Ha'azinu (52)
if i in _PAIRS and i in combos:
out.append(f"{PARSHIYOT[i]}-{PARSHIYOT[i + 1]}")
i += 2
else:
out.append(PARSHIYOT[i])
i += 1
return out
def _bereshit_shabbat(year: int, israel: bool) -> int:
"""Return the RD of the first Shabbat after Simchat Torah of ``year``."""
simchat_torah = HebrewDate(year, 7, 22 if israel else 23).to_rd()
return simchat_torah + ((6 - simchat_torah % 7) % 7) + (7 if simchat_torah % 7 == 6 else 0)
[docs]
def parasha(date: HebrewDate, israel: bool = False) -> str | None:
"""Return the parashah read on the Shabbat ``date``, or None.
None is returned when ``date`` is not a Saturday, or when the Shabbat carries a
festival reading instead of the weekly parashah.
"""
rd = date.to_rd()
if rd % 7 != 6:
return None
displaced = _displaced(israel)
if (date.month, date.day) in displaced:
return None
cycle_year = date.year if rd >= _bereshit_shabbat(date.year, israel) else date.year - 1
readings = _readings(cycle_year, israel)
index = 0
cur = _bereshit_shabbat(cycle_year, israel)
while cur <= rd:
cur_date = HebrewDate.from_rd(cur)
if (cur_date.month, cur_date.day) in displaced:
cur += 7
continue
if cur == rd:
return readings[index] if index < len(readings) else None
index += 1
if index >= len(readings):
return None
cur += 7
return None
[docs]
def triennial_portion(date: HebrewDate, israel: bool = False) -> int | None:
"""Return which third (1, 2 or 3) of the weekly parashah is read under the common
triennial cycle, or None if there is no weekly parashah that Shabbat."""
if parasha(date, israel) is None:
return None
rd = date.to_rd()
cycle_year = date.year if rd >= _bereshit_shabbat(date.year, israel) else date.year - 1
return cycle_year % 3 + 1