us-equity-market-calendar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moshe Malka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # us-equity-market-calendar
2
+
3
+ The **NYSE / NASDAQ** trading calendar — holidays (including Good Friday and Juneteenth), **1:00 p.m. early closes**, trading-day navigation, and a DST-aware **is-market-open**. Algorithmic, zero dependencies — every past and future year is correct.
4
+
5
+ ```
6
+ npm install us-equity-market-calendar
7
+ ```
8
+
9
+ ## Why
10
+
11
+ Python has `pandas_market_calendars`; JavaScript had no maintained equivalent. Existing npm packages give you federal holidays or a stale hard-coded list, but not the *equity* calendar with its early closes and session hours. This computes it from the rules, so it's right for any year.
12
+
13
+ ```ts
14
+ import { isTradingDay, nextTradingDay, isMarketOpen, sessionClose } from "us-equity-market-calendar";
15
+
16
+ isTradingDay("2026-10-12"); // true — Columbus Day: stocks trade (bonds don't)
17
+ isTradingDay("2026-04-03"); // false — Good Friday: stocks closed (bonds don't)
18
+
19
+ nextTradingDay("2026-07-02"); // "2026-07-06" — skips the Jul 3 holiday + weekend
20
+ sessionClose("2026-12-24"); // "13:00" — Christmas Eve early close
21
+ isMarketOpen(new Date()); // true/false, DST-correct
22
+ ```
23
+
24
+ ## API
25
+
26
+ Dates are `"YYYY-MM-DD"` strings or `Date` objects (UTC calendar dates).
27
+
28
+ - **`getHolidays(year)`** → `{ date, name }[]` — full closures.
29
+ - **`getEarlyCloses(year)`** → `{ date, name, closeTimeET: "13:00" }[]`.
30
+ - **`isHoliday(date)`** / **`isEarlyClose(date)`** → the entry or `null`.
31
+ - **`isTradingDay(date)`** — weekday and not a full closure.
32
+ - **`nextTradingDay` / `previousTradingDay` / `addTradingDays(date, n)` / `countTradingDays(from, to)`**.
33
+ - **`sessionClose(date)`** → `"16:00" | "13:00" | null`.
34
+ - **`isMarketOpen(instant?)`** — is the regular session (09:30–16:00 ET, 13:00 on early closes) open at a `Date`, with America/New_York DST handled via the built-in `Intl` API.
35
+
36
+ Out-of-range years and malformed dates throw `RangeError`.
37
+
38
+ ## Correctness
39
+
40
+ Full-closure and early-close output is verified against the **published NYSE calendar for 2025–2028** — including Good Friday, the Juneteenth start in 2022, the Saturday-New-Year non-observance rule, and the exact early-close pattern (e.g. no July 3 early close in 2026 because it's the observed Independence Day). The rules are algorithmic (nth-weekday, Easter computus, weekend observance), so the same logic extends to any year; intraday open/close is checked across both EST and EDT.
41
+
42
+ ## Related
43
+
44
+ The bond-market counterpart is [`sifma-holidays`](https://github.com/moshejs/sifma-holidays) (Columbus/Veterans Day closes, 2 p.m. early closes, T+1 settlement). Part of a markets toolkit: [`fx-value-date`](https://github.com/moshejs/fx-value-date) · [`32nds`](https://github.com/moshejs/32nds) · [`day-count`](https://github.com/moshejs/day-count) · [`newyorkfed`](https://github.com/moshejs/newyorkfed).
45
+
46
+ ## Author
47
+
48
+ Built by **[Moshe Malka](https://moshemalka.com)** — engineering leader in New York City. Studio work at [Quentin.Code](https://www.quentin.software/).
49
+
50
+ MIT © Moshe Malka
package/dist/index.cjs ADDED
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ addTradingDays: () => addTradingDays,
24
+ countTradingDays: () => countTradingDays,
25
+ getEarlyCloses: () => getEarlyCloses,
26
+ getHolidays: () => getHolidays,
27
+ isEarlyClose: () => isEarlyClose,
28
+ isHoliday: () => isHoliday,
29
+ isMarketOpen: () => isMarketOpen,
30
+ isTradingDay: () => isTradingDay,
31
+ nextTradingDay: () => nextTradingDay,
32
+ previousTradingDay: () => previousTradingDay,
33
+ sessionClose: () => sessionClose
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
37
+ function toYMD(input, label = "date") {
38
+ if (input instanceof Date) {
39
+ if (Number.isNaN(input.getTime())) throw new RangeError(`Invalid Date for ${label}`);
40
+ return { y: input.getUTCFullYear(), m: input.getUTCMonth() + 1, d: input.getUTCDate() };
41
+ }
42
+ const m = DATE_RE.exec(input);
43
+ if (!m) throw new RangeError(`Invalid date for ${label}: "${input}" (expected YYYY-MM-DD)`);
44
+ const ymd = { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
45
+ const check = new Date(Date.UTC(ymd.y, ymd.m - 1, ymd.d));
46
+ if (check.getUTCFullYear() !== ymd.y || check.getUTCMonth() + 1 !== ymd.m || check.getUTCDate() !== ymd.d) {
47
+ throw new RangeError(`Invalid calendar date for ${label}: "${input}"`);
48
+ }
49
+ return ymd;
50
+ }
51
+ function iso(a) {
52
+ return `${String(a.y).padStart(4, "0")}-${String(a.m).padStart(2, "0")}-${String(a.d).padStart(2, "0")}`;
53
+ }
54
+ function dow(a) {
55
+ return new Date(Date.UTC(a.y, a.m - 1, a.d)).getUTCDay();
56
+ }
57
+ function addDays(a, n) {
58
+ const t = new Date(Date.UTC(a.y, a.m - 1, a.d + n));
59
+ return { y: t.getUTCFullYear(), m: t.getUTCMonth() + 1, d: t.getUTCDate() };
60
+ }
61
+ function nthWeekday(y, m, weekday, n) {
62
+ const first = dow({ y, m, d: 1 });
63
+ return { y, m, d: 1 + (weekday - first + 7) % 7 + (n - 1) * 7 };
64
+ }
65
+ function lastWeekday(y, m, weekday) {
66
+ const lastDay = new Date(Date.UTC(y, m, 0)).getUTCDate();
67
+ const last = dow({ y, m, d: lastDay });
68
+ return { y, m, d: lastDay - (last - weekday + 7) % 7 };
69
+ }
70
+ function easterSunday(y) {
71
+ const a = y % 19;
72
+ const b = Math.floor(y / 100);
73
+ const c = y % 100;
74
+ const d = Math.floor(b / 4);
75
+ const e = b % 4;
76
+ const f = Math.floor((b + 8) / 25);
77
+ const g = Math.floor((b - f + 1) / 3);
78
+ const h = (19 * a + b - d - g + 15) % 30;
79
+ const i = Math.floor(c / 4);
80
+ const k = c % 4;
81
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
82
+ const mm = Math.floor((a + 11 * h + 22 * l) / 451);
83
+ const month = Math.floor((h + l - 7 * mm + 114) / 31);
84
+ const day = (h + l - 7 * mm + 114) % 31 + 1;
85
+ return { y, m: month, d: day };
86
+ }
87
+ function observed(a, isNewYear = false) {
88
+ const w = dow(a);
89
+ if (w === 6) return isNewYear ? null : addDays(a, -1);
90
+ if (w === 0) return addDays(a, 1);
91
+ return a;
92
+ }
93
+ var JUNETEENTH_FROM = 2022;
94
+ function holidaysForYear(year) {
95
+ const items = [];
96
+ const add = (d, name) => {
97
+ if (d && d.y === year) items.push({ date: iso(d), name });
98
+ };
99
+ add(observed({ y: year, m: 1, d: 1 }, true), "New Year's Day");
100
+ add(nthWeekday(year, 1, 1, 3), "Martin Luther King Jr. Day");
101
+ add(nthWeekday(year, 2, 1, 3), "Washington's Birthday");
102
+ add(addDays(easterSunday(year), -2), "Good Friday");
103
+ add(lastWeekday(year, 5, 1), "Memorial Day");
104
+ if (year >= JUNETEENTH_FROM) add(observed({ y: year, m: 6, d: 19 }), "Juneteenth");
105
+ add(observed({ y: year, m: 7, d: 4 }), "Independence Day");
106
+ add(nthWeekday(year, 9, 1, 1), "Labor Day");
107
+ add(nthWeekday(year, 11, 4, 4), "Thanksgiving Day");
108
+ add(observed({ y: year, m: 12, d: 25 }), "Christmas Day");
109
+ const out = items.filter((h) => h !== null);
110
+ out.sort((a, b) => a.date < b.date ? -1 : 1);
111
+ return out;
112
+ }
113
+ function earlyClosesForYear(year) {
114
+ const out = [];
115
+ const thanksgiving = nthWeekday(year, 11, 4, 4);
116
+ out.push({ date: iso(addDays(thanksgiving, 1)), name: "Day after Thanksgiving", closeTimeET: "13:00" });
117
+ const jul4 = dow({ y: year, m: 7, d: 4 });
118
+ if (jul4 >= 2 && jul4 <= 5) {
119
+ out.push({ date: iso({ y: year, m: 7, d: 3 }), name: "Independence Day (early close)", closeTimeET: "13:00" });
120
+ }
121
+ const dec25 = dow({ y: year, m: 12, d: 25 });
122
+ if (dec25 >= 2 && dec25 <= 5) {
123
+ out.push({ date: iso({ y: year, m: 12, d: 24 }), name: "Christmas Eve (early close)", closeTimeET: "13:00" });
124
+ }
125
+ out.sort((a, b) => a.date < b.date ? -1 : 1);
126
+ return out;
127
+ }
128
+ var holidayCache = /* @__PURE__ */ new Map();
129
+ var earlyCache = /* @__PURE__ */ new Map();
130
+ function holidayMap(year) {
131
+ let m = holidayCache.get(year);
132
+ if (!m) {
133
+ m = new Map(holidaysForYear(year).map((h) => [h.date, h.name]));
134
+ holidayCache.set(year, m);
135
+ }
136
+ return m;
137
+ }
138
+ function earlyMap(year) {
139
+ let m = earlyCache.get(year);
140
+ if (!m) {
141
+ m = new Map(earlyClosesForYear(year).map((e) => [e.date, e]));
142
+ earlyCache.set(year, m);
143
+ }
144
+ return m;
145
+ }
146
+ function checkYear(year) {
147
+ if (!Number.isInteger(year) || year < 1900 || year > 2200) {
148
+ throw new RangeError(`Year out of supported range (1900\u20132200): ${year}`);
149
+ }
150
+ }
151
+ function getHolidays(year) {
152
+ checkYear(year);
153
+ return holidaysForYear(year);
154
+ }
155
+ function getEarlyCloses(year) {
156
+ checkYear(year);
157
+ return earlyClosesForYear(year);
158
+ }
159
+ function isWeekend(a) {
160
+ const w = dow(a);
161
+ return w === 0 || w === 6;
162
+ }
163
+ function isHoliday(date) {
164
+ const a = toYMD(date);
165
+ const name = holidayMap(a.y).get(iso(a));
166
+ return name ? { date: iso(a), name } : null;
167
+ }
168
+ function isEarlyClose(date) {
169
+ const a = toYMD(date);
170
+ return earlyMap(a.y).get(iso(a)) ?? null;
171
+ }
172
+ function isTradingDay(date) {
173
+ const a = toYMD(date);
174
+ return !isWeekend(a) && !holidayMap(a.y).has(iso(a));
175
+ }
176
+ function nextTradingDay(date) {
177
+ let cur = addDays(toYMD(date), 1);
178
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, 1);
179
+ return iso(cur);
180
+ }
181
+ function previousTradingDay(date) {
182
+ let cur = addDays(toYMD(date), -1);
183
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, -1);
184
+ return iso(cur);
185
+ }
186
+ function addTradingDays(date, n) {
187
+ if (!Number.isInteger(n)) throw new RangeError(`n must be an integer, got ${n}`);
188
+ let cur = toYMD(date);
189
+ const step = n < 0 ? -1 : 1;
190
+ for (let left = Math.abs(n); left > 0; left--) {
191
+ cur = addDays(cur, step);
192
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, step);
193
+ }
194
+ return iso(cur);
195
+ }
196
+ function countTradingDays(from, to) {
197
+ let a = toYMD(from);
198
+ const b = toYMD(to);
199
+ if (iso(a) > iso(b)) throw new RangeError("`from` must be on or before `to`");
200
+ let count = 0;
201
+ while (iso(a) <= iso(b)) {
202
+ if (isTradingDay(iso(a))) count++;
203
+ a = addDays(a, 1);
204
+ }
205
+ return count;
206
+ }
207
+ function sessionClose(date) {
208
+ if (!isTradingDay(date)) return null;
209
+ return isEarlyClose(date) ? "13:00" : "16:00";
210
+ }
211
+ function toEasternParts(instant) {
212
+ const fmt = new Intl.DateTimeFormat("en-US", {
213
+ timeZone: "America/New_York",
214
+ year: "numeric",
215
+ month: "2-digit",
216
+ day: "2-digit",
217
+ hour: "2-digit",
218
+ minute: "2-digit",
219
+ hour12: false
220
+ });
221
+ const parts = fmt.formatToParts(instant);
222
+ const get = (t) => Number(parts.find((p) => p.type === t).value);
223
+ let hour = get("hour");
224
+ if (hour === 24) hour = 0;
225
+ return { y: get("year"), m: get("month"), d: get("day"), hour, minute: get("minute") };
226
+ }
227
+ function isMarketOpen(instant = /* @__PURE__ */ new Date()) {
228
+ if (!(instant instanceof Date) || Number.isNaN(instant.getTime())) {
229
+ throw new RangeError("isMarketOpen requires a valid Date");
230
+ }
231
+ const et = toEasternParts(instant);
232
+ const dateStr = iso({ y: et.y, m: et.m, d: et.d });
233
+ if (!isTradingDay(dateStr)) return false;
234
+ const minutes = et.hour * 60 + et.minute;
235
+ const open = 9 * 60 + 30;
236
+ const close = isEarlyClose(dateStr) ? 13 * 60 : 16 * 60;
237
+ return minutes >= open && minutes < close;
238
+ }
239
+ // Annotate the CommonJS export names for ESM import in node:
240
+ 0 && (module.exports = {
241
+ addTradingDays,
242
+ countTradingDays,
243
+ getEarlyCloses,
244
+ getHolidays,
245
+ isEarlyClose,
246
+ isHoliday,
247
+ isMarketOpen,
248
+ isTradingDay,
249
+ nextTradingDay,
250
+ previousTradingDay,
251
+ sessionClose
252
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * us-equity-market-calendar — the NYSE / NASDAQ trading calendar.
3
+ *
4
+ * Full-closure holidays (including Good Friday and Juneteenth from 2022),
5
+ * 1:00 p.m. early-close half-days, weekend observance, and session-aware
6
+ * open/closed queries with US-Eastern DST handling. Zero dependencies — the
7
+ * holiday rules are algorithmic, so every past and future year is correct.
8
+ *
9
+ * This is the STOCK market calendar: it closes on Good Friday (unlike the bond
10
+ * market) and does NOT close on Columbus Day or Veterans Day (unlike the bond
11
+ * market). Regular session 09:30–16:00 ET; early closes end 13:00 ET.
12
+ */
13
+ type DateInput = string | Date;
14
+ interface MarketHoliday {
15
+ date: string;
16
+ name: string;
17
+ }
18
+ interface EarlyClose {
19
+ date: string;
20
+ name: string;
21
+ /** Session close, US Eastern Time. Equity early closes are 13:00. */
22
+ closeTimeET: "13:00";
23
+ }
24
+ /** All NYSE/NASDAQ full-closure holidays for a year, sorted by date. */
25
+ declare function getHolidays(year: number): MarketHoliday[];
26
+ /** All 1 p.m. early-close half-days for a year, sorted by date. */
27
+ declare function getEarlyCloses(year: number): EarlyClose[];
28
+ /** The holiday entry for a date, or null. */
29
+ declare function isHoliday(date: DateInput): MarketHoliday | null;
30
+ /** The early-close entry for a date, or null. Early-close days ARE trading days. */
31
+ declare function isEarlyClose(date: DateInput): EarlyClose | null;
32
+ /** True when the market trades that day: a weekday that is not a full closure. */
33
+ declare function isTradingDay(date: DateInput): boolean;
34
+ /** The next trading day strictly after `date`. */
35
+ declare function nextTradingDay(date: DateInput): string;
36
+ /** The previous trading day strictly before `date`. */
37
+ declare function previousTradingDay(date: DateInput): string;
38
+ /** Add `n` trading days (n may be negative; 0 returns the date unchanged). */
39
+ declare function addTradingDays(date: DateInput, n: number): string;
40
+ /** Count trading days in [from, to] inclusive of neither endpoint's non-trading days. */
41
+ declare function countTradingDays(from: DateInput, to: DateInput): number;
42
+ /**
43
+ * The regular-session close time for a trading day, in ET: "16:00" normally,
44
+ * "13:00" on an early-close half-day. Returns null on a non-trading day.
45
+ */
46
+ declare function sessionClose(date: DateInput): "16:00" | "13:00" | null;
47
+ /**
48
+ * Is the regular equity session open at the given instant? Regular hours are
49
+ * 09:30–16:00 ET (until 13:00 on early-close days). DST is handled via the
50
+ * America/New_York time zone.
51
+ */
52
+ declare function isMarketOpen(instant?: Date): boolean;
53
+
54
+ export { type DateInput, type EarlyClose, type MarketHoliday, addTradingDays, countTradingDays, getEarlyCloses, getHolidays, isEarlyClose, isHoliday, isMarketOpen, isTradingDay, nextTradingDay, previousTradingDay, sessionClose };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * us-equity-market-calendar — the NYSE / NASDAQ trading calendar.
3
+ *
4
+ * Full-closure holidays (including Good Friday and Juneteenth from 2022),
5
+ * 1:00 p.m. early-close half-days, weekend observance, and session-aware
6
+ * open/closed queries with US-Eastern DST handling. Zero dependencies — the
7
+ * holiday rules are algorithmic, so every past and future year is correct.
8
+ *
9
+ * This is the STOCK market calendar: it closes on Good Friday (unlike the bond
10
+ * market) and does NOT close on Columbus Day or Veterans Day (unlike the bond
11
+ * market). Regular session 09:30–16:00 ET; early closes end 13:00 ET.
12
+ */
13
+ type DateInput = string | Date;
14
+ interface MarketHoliday {
15
+ date: string;
16
+ name: string;
17
+ }
18
+ interface EarlyClose {
19
+ date: string;
20
+ name: string;
21
+ /** Session close, US Eastern Time. Equity early closes are 13:00. */
22
+ closeTimeET: "13:00";
23
+ }
24
+ /** All NYSE/NASDAQ full-closure holidays for a year, sorted by date. */
25
+ declare function getHolidays(year: number): MarketHoliday[];
26
+ /** All 1 p.m. early-close half-days for a year, sorted by date. */
27
+ declare function getEarlyCloses(year: number): EarlyClose[];
28
+ /** The holiday entry for a date, or null. */
29
+ declare function isHoliday(date: DateInput): MarketHoliday | null;
30
+ /** The early-close entry for a date, or null. Early-close days ARE trading days. */
31
+ declare function isEarlyClose(date: DateInput): EarlyClose | null;
32
+ /** True when the market trades that day: a weekday that is not a full closure. */
33
+ declare function isTradingDay(date: DateInput): boolean;
34
+ /** The next trading day strictly after `date`. */
35
+ declare function nextTradingDay(date: DateInput): string;
36
+ /** The previous trading day strictly before `date`. */
37
+ declare function previousTradingDay(date: DateInput): string;
38
+ /** Add `n` trading days (n may be negative; 0 returns the date unchanged). */
39
+ declare function addTradingDays(date: DateInput, n: number): string;
40
+ /** Count trading days in [from, to] inclusive of neither endpoint's non-trading days. */
41
+ declare function countTradingDays(from: DateInput, to: DateInput): number;
42
+ /**
43
+ * The regular-session close time for a trading day, in ET: "16:00" normally,
44
+ * "13:00" on an early-close half-day. Returns null on a non-trading day.
45
+ */
46
+ declare function sessionClose(date: DateInput): "16:00" | "13:00" | null;
47
+ /**
48
+ * Is the regular equity session open at the given instant? Regular hours are
49
+ * 09:30–16:00 ET (until 13:00 on early-close days). DST is handled via the
50
+ * America/New_York time zone.
51
+ */
52
+ declare function isMarketOpen(instant?: Date): boolean;
53
+
54
+ export { type DateInput, type EarlyClose, type MarketHoliday, addTradingDays, countTradingDays, getEarlyCloses, getHolidays, isEarlyClose, isHoliday, isMarketOpen, isTradingDay, nextTradingDay, previousTradingDay, sessionClose };
package/dist/index.js ADDED
@@ -0,0 +1,217 @@
1
+ // src/index.ts
2
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
3
+ function toYMD(input, label = "date") {
4
+ if (input instanceof Date) {
5
+ if (Number.isNaN(input.getTime())) throw new RangeError(`Invalid Date for ${label}`);
6
+ return { y: input.getUTCFullYear(), m: input.getUTCMonth() + 1, d: input.getUTCDate() };
7
+ }
8
+ const m = DATE_RE.exec(input);
9
+ if (!m) throw new RangeError(`Invalid date for ${label}: "${input}" (expected YYYY-MM-DD)`);
10
+ const ymd = { y: Number(m[1]), m: Number(m[2]), d: Number(m[3]) };
11
+ const check = new Date(Date.UTC(ymd.y, ymd.m - 1, ymd.d));
12
+ if (check.getUTCFullYear() !== ymd.y || check.getUTCMonth() + 1 !== ymd.m || check.getUTCDate() !== ymd.d) {
13
+ throw new RangeError(`Invalid calendar date for ${label}: "${input}"`);
14
+ }
15
+ return ymd;
16
+ }
17
+ function iso(a) {
18
+ return `${String(a.y).padStart(4, "0")}-${String(a.m).padStart(2, "0")}-${String(a.d).padStart(2, "0")}`;
19
+ }
20
+ function dow(a) {
21
+ return new Date(Date.UTC(a.y, a.m - 1, a.d)).getUTCDay();
22
+ }
23
+ function addDays(a, n) {
24
+ const t = new Date(Date.UTC(a.y, a.m - 1, a.d + n));
25
+ return { y: t.getUTCFullYear(), m: t.getUTCMonth() + 1, d: t.getUTCDate() };
26
+ }
27
+ function nthWeekday(y, m, weekday, n) {
28
+ const first = dow({ y, m, d: 1 });
29
+ return { y, m, d: 1 + (weekday - first + 7) % 7 + (n - 1) * 7 };
30
+ }
31
+ function lastWeekday(y, m, weekday) {
32
+ const lastDay = new Date(Date.UTC(y, m, 0)).getUTCDate();
33
+ const last = dow({ y, m, d: lastDay });
34
+ return { y, m, d: lastDay - (last - weekday + 7) % 7 };
35
+ }
36
+ function easterSunday(y) {
37
+ const a = y % 19;
38
+ const b = Math.floor(y / 100);
39
+ const c = y % 100;
40
+ const d = Math.floor(b / 4);
41
+ const e = b % 4;
42
+ const f = Math.floor((b + 8) / 25);
43
+ const g = Math.floor((b - f + 1) / 3);
44
+ const h = (19 * a + b - d - g + 15) % 30;
45
+ const i = Math.floor(c / 4);
46
+ const k = c % 4;
47
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
48
+ const mm = Math.floor((a + 11 * h + 22 * l) / 451);
49
+ const month = Math.floor((h + l - 7 * mm + 114) / 31);
50
+ const day = (h + l - 7 * mm + 114) % 31 + 1;
51
+ return { y, m: month, d: day };
52
+ }
53
+ function observed(a, isNewYear = false) {
54
+ const w = dow(a);
55
+ if (w === 6) return isNewYear ? null : addDays(a, -1);
56
+ if (w === 0) return addDays(a, 1);
57
+ return a;
58
+ }
59
+ var JUNETEENTH_FROM = 2022;
60
+ function holidaysForYear(year) {
61
+ const items = [];
62
+ const add = (d, name) => {
63
+ if (d && d.y === year) items.push({ date: iso(d), name });
64
+ };
65
+ add(observed({ y: year, m: 1, d: 1 }, true), "New Year's Day");
66
+ add(nthWeekday(year, 1, 1, 3), "Martin Luther King Jr. Day");
67
+ add(nthWeekday(year, 2, 1, 3), "Washington's Birthday");
68
+ add(addDays(easterSunday(year), -2), "Good Friday");
69
+ add(lastWeekday(year, 5, 1), "Memorial Day");
70
+ if (year >= JUNETEENTH_FROM) add(observed({ y: year, m: 6, d: 19 }), "Juneteenth");
71
+ add(observed({ y: year, m: 7, d: 4 }), "Independence Day");
72
+ add(nthWeekday(year, 9, 1, 1), "Labor Day");
73
+ add(nthWeekday(year, 11, 4, 4), "Thanksgiving Day");
74
+ add(observed({ y: year, m: 12, d: 25 }), "Christmas Day");
75
+ const out = items.filter((h) => h !== null);
76
+ out.sort((a, b) => a.date < b.date ? -1 : 1);
77
+ return out;
78
+ }
79
+ function earlyClosesForYear(year) {
80
+ const out = [];
81
+ const thanksgiving = nthWeekday(year, 11, 4, 4);
82
+ out.push({ date: iso(addDays(thanksgiving, 1)), name: "Day after Thanksgiving", closeTimeET: "13:00" });
83
+ const jul4 = dow({ y: year, m: 7, d: 4 });
84
+ if (jul4 >= 2 && jul4 <= 5) {
85
+ out.push({ date: iso({ y: year, m: 7, d: 3 }), name: "Independence Day (early close)", closeTimeET: "13:00" });
86
+ }
87
+ const dec25 = dow({ y: year, m: 12, d: 25 });
88
+ if (dec25 >= 2 && dec25 <= 5) {
89
+ out.push({ date: iso({ y: year, m: 12, d: 24 }), name: "Christmas Eve (early close)", closeTimeET: "13:00" });
90
+ }
91
+ out.sort((a, b) => a.date < b.date ? -1 : 1);
92
+ return out;
93
+ }
94
+ var holidayCache = /* @__PURE__ */ new Map();
95
+ var earlyCache = /* @__PURE__ */ new Map();
96
+ function holidayMap(year) {
97
+ let m = holidayCache.get(year);
98
+ if (!m) {
99
+ m = new Map(holidaysForYear(year).map((h) => [h.date, h.name]));
100
+ holidayCache.set(year, m);
101
+ }
102
+ return m;
103
+ }
104
+ function earlyMap(year) {
105
+ let m = earlyCache.get(year);
106
+ if (!m) {
107
+ m = new Map(earlyClosesForYear(year).map((e) => [e.date, e]));
108
+ earlyCache.set(year, m);
109
+ }
110
+ return m;
111
+ }
112
+ function checkYear(year) {
113
+ if (!Number.isInteger(year) || year < 1900 || year > 2200) {
114
+ throw new RangeError(`Year out of supported range (1900\u20132200): ${year}`);
115
+ }
116
+ }
117
+ function getHolidays(year) {
118
+ checkYear(year);
119
+ return holidaysForYear(year);
120
+ }
121
+ function getEarlyCloses(year) {
122
+ checkYear(year);
123
+ return earlyClosesForYear(year);
124
+ }
125
+ function isWeekend(a) {
126
+ const w = dow(a);
127
+ return w === 0 || w === 6;
128
+ }
129
+ function isHoliday(date) {
130
+ const a = toYMD(date);
131
+ const name = holidayMap(a.y).get(iso(a));
132
+ return name ? { date: iso(a), name } : null;
133
+ }
134
+ function isEarlyClose(date) {
135
+ const a = toYMD(date);
136
+ return earlyMap(a.y).get(iso(a)) ?? null;
137
+ }
138
+ function isTradingDay(date) {
139
+ const a = toYMD(date);
140
+ return !isWeekend(a) && !holidayMap(a.y).has(iso(a));
141
+ }
142
+ function nextTradingDay(date) {
143
+ let cur = addDays(toYMD(date), 1);
144
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, 1);
145
+ return iso(cur);
146
+ }
147
+ function previousTradingDay(date) {
148
+ let cur = addDays(toYMD(date), -1);
149
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, -1);
150
+ return iso(cur);
151
+ }
152
+ function addTradingDays(date, n) {
153
+ if (!Number.isInteger(n)) throw new RangeError(`n must be an integer, got ${n}`);
154
+ let cur = toYMD(date);
155
+ const step = n < 0 ? -1 : 1;
156
+ for (let left = Math.abs(n); left > 0; left--) {
157
+ cur = addDays(cur, step);
158
+ while (!isTradingDay(iso(cur))) cur = addDays(cur, step);
159
+ }
160
+ return iso(cur);
161
+ }
162
+ function countTradingDays(from, to) {
163
+ let a = toYMD(from);
164
+ const b = toYMD(to);
165
+ if (iso(a) > iso(b)) throw new RangeError("`from` must be on or before `to`");
166
+ let count = 0;
167
+ while (iso(a) <= iso(b)) {
168
+ if (isTradingDay(iso(a))) count++;
169
+ a = addDays(a, 1);
170
+ }
171
+ return count;
172
+ }
173
+ function sessionClose(date) {
174
+ if (!isTradingDay(date)) return null;
175
+ return isEarlyClose(date) ? "13:00" : "16:00";
176
+ }
177
+ function toEasternParts(instant) {
178
+ const fmt = new Intl.DateTimeFormat("en-US", {
179
+ timeZone: "America/New_York",
180
+ year: "numeric",
181
+ month: "2-digit",
182
+ day: "2-digit",
183
+ hour: "2-digit",
184
+ minute: "2-digit",
185
+ hour12: false
186
+ });
187
+ const parts = fmt.formatToParts(instant);
188
+ const get = (t) => Number(parts.find((p) => p.type === t).value);
189
+ let hour = get("hour");
190
+ if (hour === 24) hour = 0;
191
+ return { y: get("year"), m: get("month"), d: get("day"), hour, minute: get("minute") };
192
+ }
193
+ function isMarketOpen(instant = /* @__PURE__ */ new Date()) {
194
+ if (!(instant instanceof Date) || Number.isNaN(instant.getTime())) {
195
+ throw new RangeError("isMarketOpen requires a valid Date");
196
+ }
197
+ const et = toEasternParts(instant);
198
+ const dateStr = iso({ y: et.y, m: et.m, d: et.d });
199
+ if (!isTradingDay(dateStr)) return false;
200
+ const minutes = et.hour * 60 + et.minute;
201
+ const open = 9 * 60 + 30;
202
+ const close = isEarlyClose(dateStr) ? 13 * 60 : 16 * 60;
203
+ return minutes >= open && minutes < close;
204
+ }
205
+ export {
206
+ addTradingDays,
207
+ countTradingDays,
208
+ getEarlyCloses,
209
+ getHolidays,
210
+ isEarlyClose,
211
+ isHoliday,
212
+ isMarketOpen,
213
+ isTradingDay,
214
+ nextTradingDay,
215
+ previousTradingDay,
216
+ sessionClose
217
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "us-equity-market-calendar",
3
+ "version": "1.0.0",
4
+ "description": "NYSE / NASDAQ trading calendar — holidays (incl. Good Friday & Juneteenth), 1pm early closes, trading-day navigation, and DST-aware is-market-open. Algorithmic, zero dependencies.",
5
+ "keywords": [
6
+ "nyse",
7
+ "nasdaq",
8
+ "trading-calendar",
9
+ "market-calendar",
10
+ "market-holidays",
11
+ "trading-days",
12
+ "is-market-open",
13
+ "early-close",
14
+ "stock-market",
15
+ "finance",
16
+ "settlement"
17
+ ],
18
+ "homepage": "https://moshemalka.com",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/moshejs/us-equity-market-calendar.git"
22
+ },
23
+ "bugs": "https://github.com/moshejs/us-equity-market-calendar/issues",
24
+ "author": "Moshe Malka <hello@moshemalka.com> (https://moshemalka.com)",
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "sideEffects": false,
41
+ "scripts": {
42
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
46
+ },
47
+ "devDependencies": {
48
+ "tsup": "^8.5.0",
49
+ "typescript": "^5.8.0",
50
+ "vitest": "^3.2.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ }
55
+ }