UNPKG

8.61 kBPlain TextView Raw
1import dayjs from "dayjs";
2import { TimezoneAbbrMap, Weekday, Month } from "./types";
3
4export const TIMEZONE_ABBR_MAP: TimezoneAbbrMap = {
5 ACDT: 630,
6 ACST: 570,
7 ADT: -180,
8 AEDT: 660,
9 AEST: 600,
10 AFT: 270,
11 AKDT: -480,
12 AKST: -540,
13 ALMT: 360,
14 AMST: -180,
15 AMT: -240,
16 ANAST: 720,
17 ANAT: 720,
18 AQTT: 300,
19 ART: -180,
20 AST: -240,
21 AWDT: 540,
22 AWST: 480,
23 AZOST: 0,
24 AZOT: -60,
25 AZST: 300,
26 AZT: 240,
27 BNT: 480,
28 BOT: -240,
29 BRST: -120,
30 BRT: -180,
31 BST: 60,
32 BTT: 360,
33 CAST: 480,
34 CAT: 120,
35 CCT: 390,
36 CDT: -300,
37 CEST: 120,
38 // Note: Many sources define CET as a constant UTC+1. In common usage, however,
39 // CET usually refers to the time observed in most of Europe, be it standard time or daylight saving time.
40 CET: {
41 timezoneOffsetDuringDst: 2 * 60,
42 timezoneOffsetNonDst: 60,
43 dstStart: (year: number) => getLastWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2),
44 dstEnd: (year: number) => getLastWeekdayOfMonth(year, Month.OCTOBER, Weekday.SUNDAY, 3),
45 },
46 CHADT: 825,
47 CHAST: 765,
48 CKT: -600,
49 CLST: -180,
50 CLT: -240,
51 COT: -300,
52 CST: -360,
53 CT: {
54 timezoneOffsetDuringDst: -5 * 60,
55 timezoneOffsetNonDst: -6 * 60,
56 dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
57 dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
58 },
59 CVT: -60,
60 CXT: 420,
61 ChST: 600,
62 DAVT: 420,
63 EASST: -300,
64 EAST: -360,
65 EAT: 180,
66 ECT: -300,
67 EDT: -240,
68 EEST: 180,
69 EET: 120,
70 EGST: 0,
71 EGT: -60,
72 EST: -300,
73 ET: {
74 timezoneOffsetDuringDst: -4 * 60,
75 timezoneOffsetNonDst: -5 * 60,
76 dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
77 dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
78 },
79 FJST: 780,
80 FJT: 720,
81 FKST: -180,
82 FKT: -240,
83 FNT: -120,
84 GALT: -360,
85 GAMT: -540,
86 GET: 240,
87 GFT: -180,
88 GILT: 720,
89 GMT: 0,
90 GST: 240,
91 GYT: -240,
92 HAA: -180,
93 HAC: -300,
94 HADT: -540,
95 HAE: -240,
96 HAP: -420,
97 HAR: -360,
98 HAST: -600,
99 HAT: -90,
100 HAY: -480,
101 HKT: 480,
102 HLV: -210,
103 HNA: -240,
104 HNC: -360,
105 HNE: -300,
106 HNP: -480,
107 HNR: -420,
108 HNT: -150,
109 HNY: -540,
110 HOVT: 420,
111 ICT: 420,
112 IDT: 180,
113 IOT: 360,
114 IRDT: 270,
115 IRKST: 540,
116 IRKT: 540,
117 IRST: 210,
118 IST: 330,
119 JST: 540,
120 KGT: 360,
121 KRAST: 480,
122 KRAT: 480,
123 KST: 540,
124 KUYT: 240,
125 LHDT: 660,
126 LHST: 630,
127 LINT: 840,
128 MAGST: 720,
129 MAGT: 720,
130 MART: -510,
131 MAWT: 300,
132 MDT: -360,
133 MESZ: 120,
134 MEZ: 60,
135 MHT: 720,
136 MMT: 390,
137 MSD: 240,
138 MSK: 180,
139 MST: -420,
140 MT: {
141 timezoneOffsetDuringDst: -6 * 60,
142 timezoneOffsetNonDst: -7 * 60,
143 dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
144 dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
145 },
146 MUT: 240,
147 MVT: 300,
148 MYT: 480,
149 NCT: 660,
150 NDT: -90,
151 NFT: 690,
152 NOVST: 420,
153 NOVT: 360,
154 NPT: 345,
155 NST: -150,
156 NUT: -660,
157 NZDT: 780,
158 NZST: 720,
159 OMSST: 420,
160 OMST: 420,
161 PDT: -420,
162 PET: -300,
163 PETST: 720,
164 PETT: 720,
165 PGT: 600,
166 PHOT: 780,
167 PHT: 480,
168 PKT: 300,
169 PMDT: -120,
170 PMST: -180,
171 PONT: 660,
172 PST: -480,
173 PT: {
174 timezoneOffsetDuringDst: -7 * 60,
175 timezoneOffsetNonDst: -8 * 60,
176 dstStart: (year: number) => getNthWeekdayOfMonth(year, Month.MARCH, Weekday.SUNDAY, 2, 2),
177 dstEnd: (year: number) => getNthWeekdayOfMonth(year, Month.NOVEMBER, Weekday.SUNDAY, 1, 2),
178 },
179 PWT: 540,
180 PYST: -180,
181 PYT: -240,
182 RET: 240,
183 SAMT: 240,
184 SAST: 120,
185 SBT: 660,
186 SCT: 240,
187 SGT: 480,
188 SRT: -180,
189 SST: -660,
190 TAHT: -600,
191 TFT: 300,
192 TJT: 300,
193 TKT: 780,
194 TLT: 540,
195 TMT: 300,
196 TVT: 720,
197 ULAT: 480,
198 UTC: 0,
199 UYST: -120,
200 UYT: -180,
201 UZT: 300,
202 VET: -210,
203 VLAST: 660,
204 VLAT: 660,
205 VUT: 660,
206 WAST: 120,
207 WAT: 60,
208 WEST: 60,
209 WESZ: 60,
210 WET: 0,
211 WEZ: 0,
212 WFT: 720,
213 WGST: -120,
214 WGT: -180,
215 WIB: 420,
216 WIT: 540,
217 WITA: 480,
218 WST: 780,
219 WT: 0,
220 YAKST: 600,
221 YAKT: 600,
222 YAPT: 600,
223 YEKST: 360,
224 YEKT: 360,
225};
226
227/**
228 * Get the date which is the nth occurence of a given weekday in a given month and year.
229 *
230 * @param year The year for which to find the date
231 * @param month The month in which the date occurs
232 * @param weekday The weekday on which the date occurs
233 * @param n The nth occurence of the given weekday on the month to return
234 * @param hour The hour of day which should be set on the returned date
235 * @return The date which is the nth occurence of a given weekday in a given
236 * month and year, at the given hour of day
237 */
238export function getNthWeekdayOfMonth(year: number, month: Month, weekday: Weekday, n: 1 | 2 | 3 | 4, hour = 0): Date {
239 let dayOfMonth = 0;
240 let i = 0;
241 while (i < n) {
242 dayOfMonth++;
243 const date = new Date(year, month - 1, dayOfMonth);
244 if (date.getDay() === weekday) i++;
245 }
246 return new Date(year, month - 1, dayOfMonth, hour);
247}
248
249/**
250 * Get the date which is the last occurence of a given weekday in a given month and year.
251 *
252 * @param year The year for which to find the date
253 * @param month The month in which the date occurs
254 * @param weekday The weekday on which the date occurs
255 * @param hour The hour of day which should be set on the returned date
256 * @return The date which is the last occurence of a given weekday in a given
257 * month and year, at the given hour of day
258 */
259export function getLastWeekdayOfMonth(year: number, month: Month, weekday: Weekday, hour = 0): Date {
260 // Procedure: Find the first weekday of the next month, compare with the given weekday,
261 // and use the difference to determine how many days to subtract from the first of the next month.
262 const oneIndexedWeekday = weekday === 0 ? 7 : weekday;
263 const date = new Date(year, month - 1 + 1, 1, 12);
264 const firstWeekdayNextMonth = date.getDay() === 0 ? 7 : date.getDay();
265 let dayDiff;
266 if (firstWeekdayNextMonth === oneIndexedWeekday) dayDiff = 7;
267 else if (firstWeekdayNextMonth < oneIndexedWeekday) dayDiff = 7 + firstWeekdayNextMonth - oneIndexedWeekday;
268 else dayDiff = firstWeekdayNextMonth - oneIndexedWeekday;
269 date.setDate(date.getDate() - dayDiff);
270 return new Date(year, month - 1, date.getDate(), hour);
271}
272
273/**
274 * Finds and returns timezone offset. If timezoneInput is numeric, it is returned. Otherwise, look for timezone offsets
275 * in the following order: timezoneOverrides -> {@link TIMEZONE_ABBR_MAP}.
276 *
277 * @param timezoneInput Uppercase timezone abbreviation or numeric offset in minutes
278 * @param date The date to use to determine whether to return DST offsets for ambiguous timezones
279 * @param timezoneOverrides Overrides for timezones
280 * @return timezone offset in minutes
281 */
282export function toTimezoneOffset(
283 timezoneInput?: string | number,
284 date?: Date,
285 timezoneOverrides: TimezoneAbbrMap = {}
286): number | null {
287 if (timezoneInput == null) {
288 return null;
289 }
290
291 if (typeof timezoneInput === "number") {
292 return timezoneInput;
293 }
294
295 const matchedTimezone = timezoneOverrides[timezoneInput] ?? TIMEZONE_ABBR_MAP[timezoneInput];
296 if (matchedTimezone == null) {
297 return null;
298 }
299 // This means that we have matched an unambiguous timezone
300 if (typeof matchedTimezone == "number") {
301 return matchedTimezone;
302 }
303
304 // The matched timezone is an ambiguous timezone, where the offset depends on whether the context (refDate)
305 // is during daylight savings or not.
306
307 // Without refDate as context, there's no way to know if DST or non-DST offset should be used. Return null instead.
308 if (date == null) {
309 return null;
310 }
311
312 // Return DST offset if the refDate is during daylight savings
313 if (
314 dayjs(date).isAfter(matchedTimezone.dstStart(date.getFullYear())) &&
315 !dayjs(date).isAfter(matchedTimezone.dstEnd(date.getFullYear()))
316 ) {
317 return matchedTimezone.timezoneOffsetDuringDst;
318 }
319
320 // refDate is not during DST => return non-DST offset
321 return matchedTimezone.timezoneOffsetNonDst;
322}