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);
}