File size: 4,415 Bytes
cf86710 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | import {
HEBREW_EPOCH,
type HebrewMonthCode,
MONTH_SEQUENCE_COMMON,
MONTH_SEQUENCE_LEAP,
type YearType,
} from "./constants.js";
const roshHashanahCache = new Map<number, number>();
const yearLengthCache = new Map<number, number>();
/**
* Calculate the modulus that always returns a positive remainder. Useful when
* applying 19-year leap cycles.
*/
export function mod(value: number, divisor: number): number {
return ((value % divisor) + divisor) % divisor;
}
/** Determine whether a Hebrew year includes the extra Adar I month. */
export function isHebrewLeapYear(year: number): boolean {
return mod(7 * year + 1, 19) < 7;
}
/** Count lunar months elapsed since the epoch up to the start of a year. */
function monthsElapsed(year: number): number {
return Math.floor((235 * year - 234) / 19);
}
/** Compute the absolute day (relative to epoch) for the new year. */
function hebrewCalendarElapsedDays(year: number): number {
const months = monthsElapsed(year);
const parts = 204 + 793 * mod(months, 1080);
const hours = 5 + 12 * months + 793 * Math.floor(months / 1080);
const day = 1 + 29 * months + Math.floor(hours / 24);
const partsRemain = mod(hours, 24) * 1080 + parts;
let roshHashanah = day;
const leapYear = isHebrewLeapYear(year);
const lastYearLeap = isHebrewLeapYear(year - 1);
if (
partsRemain >= 19440 ||
(mod(roshHashanah, 7) === 2 && partsRemain >= 9924 && !leapYear) ||
(mod(roshHashanah, 7) === 1 && partsRemain >= 16789 && lastYearLeap)
) {
roshHashanah += 1;
}
const weekday = mod(roshHashanah, 7);
if (weekday === 0 || weekday === 3 || weekday === 5) {
roshHashanah += 1;
}
return roshHashanah;
}
/** Return the absolute day for Rosh Hashanah (cached for reuse). */
export function roshHashanah(year: number): number {
const cached = roshHashanahCache.get(year);
if (cached !== undefined) {
return cached;
}
const value = HEBREW_EPOCH + hebrewCalendarElapsedDays(year);
roshHashanahCache.set(year, value);
return value;
}
/** Total days in a Hebrew year, accounting for leap and year type. */
export function daysInHebrewYear(year: number): number {
const cached = yearLengthCache.get(year);
if (cached !== undefined) {
return cached;
}
const days = roshHashanah(year + 1) - roshHashanah(year);
yearLengthCache.set(year, days);
return days;
}
/** Classify a year as deficient, regular, or complete. */
function yearType(year: number): YearType {
const days = daysInHebrewYear(year);
const leap = isHebrewLeapYear(year);
if (leap) {
if (days === 383) return "deficient";
if (days === 384) return "regular";
return "complete";
}
if (days === 353) return "deficient";
if (days === 354) return "regular";
return "complete";
}
/** Get the sequence of month codes for a year, inserting Adar I as needed. */
function monthSequence(year: number) {
return isHebrewLeapYear(year) ? MONTH_SEQUENCE_LEAP : MONTH_SEQUENCE_COMMON;
}
/** Retrieve the canonical month code for a year and month index. */
function monthCode(year: number, monthIndex: number): HebrewMonthCode {
const sequence = monthSequence(year);
if (monthIndex < 0 || monthIndex >= sequence.length) {
throw new RangeError(`Invalid month index ${monthIndex} for year ${year}`);
}
return sequence[monthIndex];
}
/** Returns the number of months in the specified year (12 or 13). */
export function monthsInHebrewYear(year: number): number {
return monthSequence(year).length;
}
/** Number of days in a given Hebrew month (by index). */
export function daysInHebrewMonth(year: number, monthIndex: number): number {
const code = monthCode(year, monthIndex);
const type = yearType(year);
switch (code) {
case "tishrei":
return 30;
case "cheshvan":
return type === "complete" ? 30 : 29;
case "kislev":
return type === "deficient" ? 29 : 30;
case "tevet":
return 29;
case "shevat":
return 30;
case "adarI":
return 30;
case "adar":
return 29;
case "nisan":
return 30;
case "iyar":
return 29;
case "sivan":
return 30;
case "tamuz":
return 29;
case "av":
return 30;
case "elul":
return 29;
default:
return 0;
}
}
export function getMonthCode(
year: number,
monthIndex: number,
): HebrewMonthCode {
return monthCode(year, monthIndex);
}
|