@internationalized/date 3.0.0-alpha.0 → 3.0.0-alpha.1
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/package.json +4 -3
- package/src/CalendarDate.ts +302 -0
- package/src/DateFormatter.ts +190 -0
- package/src/calendars/BuddhistCalendar.ts +45 -0
- package/src/calendars/EthiopicCalendar.ts +169 -0
- package/src/calendars/GregorianCalendar.ts +114 -0
- package/src/calendars/HebrewCalendar.ts +196 -0
- package/src/calendars/IndianCalendar.ts +120 -0
- package/src/calendars/IslamicCalendar.ts +203 -0
- package/src/calendars/JapaneseCalendar.ts +160 -0
- package/src/calendars/PersianCalendar.ts +92 -0
- package/src/calendars/TaiwanCalendar.ts +70 -0
- package/src/conversion.ts +274 -0
- package/src/createCalendar.ts +54 -0
- package/src/index.ts +28 -0
- package/src/manipulation.ts +445 -0
- package/src/queries.ts +217 -0
- package/src/string.ts +180 -0
- package/src/types.ts +97 -0
- package/src/utils.ts +37 -0
- package/src/weekStartData.ts +108 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {AnyCalendarDate, AnyTime, CycleOptions, CycleTimeOptions, DateField, DateFields, Disambiguation, Duration, TimeField, TimeFields} from './types';
|
|
14
|
+
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate';
|
|
15
|
+
import {epochFromDate, fromAbsolute, toAbsolute, toCalendar, toCalendarDateTime} from './conversion';
|
|
16
|
+
import {GregorianCalendar} from './calendars/GregorianCalendar';
|
|
17
|
+
import {Mutable} from './utils';
|
|
18
|
+
|
|
19
|
+
const ONE_HOUR = 3600000;
|
|
20
|
+
|
|
21
|
+
export function add(date: CalendarDateTime, duration: Duration): CalendarDateTime;
|
|
22
|
+
export function add(date: CalendarDate, duration: Duration): CalendarDate;
|
|
23
|
+
export function add(date: CalendarDate | CalendarDateTime, duration: Duration): CalendarDate | CalendarDateTime;
|
|
24
|
+
export function add(date: CalendarDate | CalendarDateTime, duration: Duration) {
|
|
25
|
+
let mutableDate: Mutable<AnyCalendarDate> = date.copy();
|
|
26
|
+
let days = 'hour' in date ? addTimeFields(date, duration) : 0;
|
|
27
|
+
|
|
28
|
+
addYears(mutableDate, duration.years || 0);
|
|
29
|
+
if (mutableDate.calendar.balanceYearMonth) {
|
|
30
|
+
mutableDate.calendar.balanceYearMonth(mutableDate, date);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mutableDate.month += duration.months || 0;
|
|
34
|
+
|
|
35
|
+
balanceYearMonth(mutableDate);
|
|
36
|
+
constrainMonthDay(mutableDate);
|
|
37
|
+
|
|
38
|
+
mutableDate.day += (duration.weeks || 0) * 7;
|
|
39
|
+
mutableDate.day += duration.days || 0;
|
|
40
|
+
mutableDate.day += days;
|
|
41
|
+
|
|
42
|
+
balanceDay(mutableDate);
|
|
43
|
+
|
|
44
|
+
if (mutableDate.calendar.balanceDate) {
|
|
45
|
+
mutableDate.calendar.balanceDate(mutableDate);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return mutableDate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addYears(date: Mutable<AnyCalendarDate>, years: number) {
|
|
52
|
+
if (date.calendar.getYearsToAdd) {
|
|
53
|
+
years = date.calendar.getYearsToAdd(date, years);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
date.year += years;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function balanceYearMonth(date: Mutable<AnyCalendarDate>) {
|
|
60
|
+
while (date.month < 1) {
|
|
61
|
+
addYears(date, -1);
|
|
62
|
+
date.month += date.calendar.getMonthsInYear(date);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let monthsInYear = 0;
|
|
66
|
+
while (date.month > (monthsInYear = date.calendar.getMonthsInYear(date))) {
|
|
67
|
+
date.month -= monthsInYear;
|
|
68
|
+
addYears(date, 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function balanceDay(date: Mutable<AnyCalendarDate>) {
|
|
73
|
+
while (date.day < 1) {
|
|
74
|
+
date.month--;
|
|
75
|
+
balanceYearMonth(date);
|
|
76
|
+
date.day += date.calendar.getDaysInMonth(date);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
while (date.day > date.calendar.getDaysInMonth(date)) {
|
|
80
|
+
date.day -= date.calendar.getDaysInMonth(date);
|
|
81
|
+
date.month++;
|
|
82
|
+
balanceYearMonth(date);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function constrainMonthDay(date: Mutable<AnyCalendarDate>) {
|
|
87
|
+
date.month = Math.max(1, Math.min(date.calendar.getMonthsInYear(date), date.month));
|
|
88
|
+
date.day = Math.max(1, Math.min(date.calendar.getDaysInMonth(date), date.day));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function constrain(date: Mutable<AnyCalendarDate>) {
|
|
92
|
+
if (date.calendar.constrainDate) {
|
|
93
|
+
date.calendar.constrainDate(date);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
date.year = Math.max(1, Math.min(date.calendar.getYearsInEra(date), date.year));
|
|
97
|
+
constrainMonthDay(date);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function invertDuration(duration: Duration): Duration {
|
|
101
|
+
let inverseDuration = {};
|
|
102
|
+
for (let key in duration) {
|
|
103
|
+
if (typeof duration[key] === 'number') {
|
|
104
|
+
inverseDuration[key] = -duration[key];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return inverseDuration;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function subtract(date: CalendarDateTime, duration: Duration): CalendarDateTime;
|
|
112
|
+
export function subtract(date: CalendarDate, duration: Duration): CalendarDate;
|
|
113
|
+
export function subtract(date: CalendarDate | CalendarDateTime, duration: Duration): CalendarDate | CalendarDateTime {
|
|
114
|
+
return add(date, invertDuration(duration));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function set(date: CalendarDateTime, fields: DateFields): CalendarDateTime;
|
|
118
|
+
export function set(date: CalendarDate, fields: DateFields): CalendarDate;
|
|
119
|
+
export function set(date: CalendarDate | CalendarDateTime, fields: DateFields) {
|
|
120
|
+
let mutableDate: Mutable<AnyCalendarDate> = date.copy();
|
|
121
|
+
|
|
122
|
+
if (fields.era != null) {
|
|
123
|
+
mutableDate.era = fields.era;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fields.year != null) {
|
|
127
|
+
mutableDate.year = fields.year;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fields.month != null) {
|
|
131
|
+
mutableDate.month = fields.month;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (fields.day != null) {
|
|
135
|
+
mutableDate.day = fields.day;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
constrain(mutableDate);
|
|
139
|
+
return mutableDate;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setTime(value: CalendarDateTime, fields: TimeFields): CalendarDateTime;
|
|
143
|
+
export function setTime(value: Time, fields: TimeFields): Time;
|
|
144
|
+
export function setTime(value: Time | CalendarDateTime, fields: TimeFields) {
|
|
145
|
+
let mutableValue: Mutable<Time | CalendarDateTime> = value.copy();
|
|
146
|
+
|
|
147
|
+
if (fields.hour != null) {
|
|
148
|
+
mutableValue.hour = fields.hour;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (fields.minute != null) {
|
|
152
|
+
mutableValue.minute = fields.minute;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (fields.second != null) {
|
|
156
|
+
mutableValue.second = fields.second;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (fields.millisecond != null) {
|
|
160
|
+
mutableValue.millisecond = fields.millisecond;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
constrainTime(mutableValue);
|
|
164
|
+
return mutableValue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function balanceTime(time: Mutable<AnyTime>): number {
|
|
168
|
+
time.second += Math.floor(time.millisecond / 1000);
|
|
169
|
+
time.millisecond = nonNegativeMod(time.millisecond, 1000);
|
|
170
|
+
|
|
171
|
+
time.minute += Math.floor(time.second / 60);
|
|
172
|
+
time.second = nonNegativeMod(time.second, 60);
|
|
173
|
+
|
|
174
|
+
time.hour += Math.floor(time.minute / 60);
|
|
175
|
+
time.minute = nonNegativeMod(time.minute, 60);
|
|
176
|
+
|
|
177
|
+
let days = Math.floor(time.hour / 24);
|
|
178
|
+
time.hour = nonNegativeMod(time.hour, 24);
|
|
179
|
+
|
|
180
|
+
return days;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function constrainTime(time: Mutable<AnyTime>) {
|
|
184
|
+
time.millisecond = Math.max(0, Math.min(time.millisecond, 1000));
|
|
185
|
+
time.second = Math.max(0, Math.min(time.second, 59));
|
|
186
|
+
time.minute = Math.max(0, Math.min(time.minute, 59));
|
|
187
|
+
time.hour = Math.max(0, Math.min(time.hour, 23));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function nonNegativeMod(a: number, b: number) {
|
|
191
|
+
let result = a % b;
|
|
192
|
+
if (result < 0) {
|
|
193
|
+
result += b;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function addTimeFields(time: Mutable<AnyTime>, duration: Duration): number {
|
|
199
|
+
time.hour += duration.hours || 0;
|
|
200
|
+
time.minute += duration.minutes || 0;
|
|
201
|
+
time.second += duration.seconds || 0;
|
|
202
|
+
time.millisecond += duration.milliseconds || 0;
|
|
203
|
+
return balanceTime(time);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function addTime(time: Time, duration: Duration): Time {
|
|
207
|
+
let res = time.copy();
|
|
208
|
+
addTimeFields(res, duration);
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function subtractTime(time: Time, duration: Duration): Time {
|
|
213
|
+
return addTime(time, invertDuration(duration));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function cycleDate(value: CalendarDateTime, field: DateField, amount: number, options?: CycleOptions): CalendarDateTime;
|
|
217
|
+
export function cycleDate(value: CalendarDate, field: DateField, amount: number, options?: CycleOptions): CalendarDate;
|
|
218
|
+
export function cycleDate(value: CalendarDate | CalendarDateTime, field: DateField, amount: number, options?: CycleOptions) {
|
|
219
|
+
let mutable: Mutable<CalendarDate | CalendarDateTime> = value.copy();
|
|
220
|
+
|
|
221
|
+
switch (field) {
|
|
222
|
+
case 'era': {
|
|
223
|
+
let eras = value.calendar.getEras();
|
|
224
|
+
let eraIndex = eras.indexOf(value.era);
|
|
225
|
+
if (eraIndex < 0) {
|
|
226
|
+
throw new Error('Invalid era: ' + value.era);
|
|
227
|
+
}
|
|
228
|
+
eraIndex = cycleValue(eraIndex, amount, 0, eras.length - 1, options?.round);
|
|
229
|
+
mutable.era = eras[eraIndex];
|
|
230
|
+
|
|
231
|
+
// Constrain the year and other fields within the era, so the era doesn't change when we balance below.
|
|
232
|
+
constrain(mutable);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'year': {
|
|
236
|
+
if (mutable.calendar.getYearsToAdd) {
|
|
237
|
+
amount = mutable.calendar.getYearsToAdd(mutable, amount);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// The year field should not cycle within the era as that can cause weird behavior affecting other fields.
|
|
241
|
+
// We need to also allow values < 1 so that decrementing goes to the previous era. If we get -Infinity back
|
|
242
|
+
// we know we wrapped around after reaching 9999 (the maximum), so set the year back to 1.
|
|
243
|
+
mutable.year = cycleValue(value.year, amount, -Infinity, 9999, options?.round);
|
|
244
|
+
if (mutable.year === -Infinity) {
|
|
245
|
+
mutable.year = 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (mutable.calendar.balanceYearMonth) {
|
|
249
|
+
mutable.calendar.balanceYearMonth(mutable, value);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'month':
|
|
254
|
+
mutable.month = cycleValue(value.month, amount, 1, value.calendar.getMonthsInYear(value), options?.round);
|
|
255
|
+
break;
|
|
256
|
+
case 'day':
|
|
257
|
+
mutable.day = cycleValue(value.day, amount, 1, value.calendar.getDaysInMonth(value), options?.round);
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
throw new Error('Unsupported field ' + field);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (value.calendar.balanceDate) {
|
|
264
|
+
value.calendar.balanceDate(mutable);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
constrain(mutable);
|
|
268
|
+
return mutable;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function cycleTime(value: CalendarDateTime, field: TimeField, amount: number, options?: CycleTimeOptions): CalendarDateTime;
|
|
272
|
+
export function cycleTime(value: Time, field: TimeField, amount: number, options?: CycleTimeOptions): Time;
|
|
273
|
+
export function cycleTime(value: Time | CalendarDateTime, field: TimeField, amount: number, options?: CycleTimeOptions) {
|
|
274
|
+
let mutable: Mutable<Time | CalendarDateTime> = value.copy();
|
|
275
|
+
|
|
276
|
+
switch (field) {
|
|
277
|
+
case 'hour': {
|
|
278
|
+
let hours = value.hour;
|
|
279
|
+
let min = 0;
|
|
280
|
+
let max = 23;
|
|
281
|
+
if (options?.hourCycle === 12) {
|
|
282
|
+
let isPM = hours >= 12;
|
|
283
|
+
min = isPM ? 12 : 0;
|
|
284
|
+
max = isPM ? 23 : 11;
|
|
285
|
+
}
|
|
286
|
+
mutable.hour = cycleValue(hours, amount, min, max, options?.round);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 'minute':
|
|
290
|
+
mutable.minute = cycleValue(value.minute, amount, 0, 59, options?.round);
|
|
291
|
+
break;
|
|
292
|
+
case 'second':
|
|
293
|
+
mutable.second = cycleValue(value.second, amount, 0, 59, options?.round);
|
|
294
|
+
break;
|
|
295
|
+
case 'millisecond':
|
|
296
|
+
mutable.millisecond = cycleValue(value.millisecond, amount, 0, 999, options?.round);
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
throw new Error('Unsupported field ' + field);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return mutable;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function cycleValue(value: number, amount: number, min: number, max: number, round = false) {
|
|
306
|
+
if (round) {
|
|
307
|
+
value += Math.sign(amount);
|
|
308
|
+
|
|
309
|
+
if (value < min) {
|
|
310
|
+
value = max;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let div = Math.abs(amount);
|
|
314
|
+
if (amount > 0) {
|
|
315
|
+
value = Math.ceil(value / div) * div;
|
|
316
|
+
} else {
|
|
317
|
+
value = Math.floor(value / div) * div;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (value > max) {
|
|
321
|
+
value = min;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
value += amount;
|
|
325
|
+
if (value < min) {
|
|
326
|
+
value = max - (min - value - 1);
|
|
327
|
+
} else if (value > max) {
|
|
328
|
+
value = min + (value - max - 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function addZoned(dateTime: ZonedDateTime, duration: Duration): ZonedDateTime {
|
|
336
|
+
let ms: number;
|
|
337
|
+
if ((duration.years != null && duration.years !== 0) || (duration.months != null && duration.months !== 0) || (duration.days != null && duration.days !== 0)) {
|
|
338
|
+
let res = add(toCalendarDateTime(dateTime), {
|
|
339
|
+
years: duration.years,
|
|
340
|
+
months: duration.months,
|
|
341
|
+
days: duration.days
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Changing the date may change the timezone offset, so we need to recompute
|
|
345
|
+
// using the 'compatible' disambiguation.
|
|
346
|
+
ms = toAbsolute(res, dateTime.timeZone);
|
|
347
|
+
} else {
|
|
348
|
+
// Otherwise, preserve the offset of the original date.
|
|
349
|
+
ms = epochFromDate(dateTime) - dateTime.offset;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Perform time manipulation in milliseconds rather than on the original time fields to account for DST.
|
|
353
|
+
// For example, adding one hour during a DST transition may result in the hour field staying the same or
|
|
354
|
+
// skipping an hour. This results in the offset field changing value instead of the specified field.
|
|
355
|
+
ms += duration.milliseconds || 0;
|
|
356
|
+
ms += (duration.seconds || 0) * 1000;
|
|
357
|
+
ms += (duration.minutes || 0) * 60 * 1000;
|
|
358
|
+
ms += (duration.hours || 0) * 60 * 60 * 1000;
|
|
359
|
+
|
|
360
|
+
let res = fromAbsolute(ms, dateTime.timeZone);
|
|
361
|
+
return toCalendar(res, dateTime.calendar);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function subtractZoned(dateTime: ZonedDateTime, duration: Duration): ZonedDateTime {
|
|
365
|
+
return addZoned(dateTime, invertDuration(duration));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function cycleZoned(dateTime: ZonedDateTime, field: DateField | TimeField, amount: number, options?: CycleTimeOptions): ZonedDateTime {
|
|
369
|
+
// For date fields, we want the time to remain consistent and the UTC offset to potentially change to account for DST changes.
|
|
370
|
+
// For time fields, we want the time to change by the amount given. This may result in the hour field staying the same, but the UTC
|
|
371
|
+
// offset changing in the case of a backward DST transition, or skipping an hour in the case of a forward DST transition.
|
|
372
|
+
switch (field) {
|
|
373
|
+
case 'hour': {
|
|
374
|
+
let min = 0;
|
|
375
|
+
let max = 23;
|
|
376
|
+
if (options?.hourCycle === 12) {
|
|
377
|
+
let isPM = dateTime.hour >= 12;
|
|
378
|
+
min = isPM ? 12 : 0;
|
|
379
|
+
max = isPM ? 23 : 11;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The minimum and maximum hour may be affected by daylight saving time.
|
|
383
|
+
// For example, it might jump forward at midnight, and skip 1am.
|
|
384
|
+
// Or it might end at midnight and repeat the 11pm hour. To handle this, we get
|
|
385
|
+
// the possible absolute times for the min and max, and find the maximum range
|
|
386
|
+
// that is within the current day.
|
|
387
|
+
let plainDateTime = toCalendarDateTime(dateTime);
|
|
388
|
+
let minDate = toCalendar(setTime(plainDateTime, {hour: min}), new GregorianCalendar());
|
|
389
|
+
let minAbsolute = [toAbsolute(minDate, dateTime.timeZone, 'earlier'), toAbsolute(minDate, dateTime.timeZone, 'later')]
|
|
390
|
+
.filter(ms => fromAbsolute(ms, dateTime.timeZone).day === minDate.day)[0];
|
|
391
|
+
|
|
392
|
+
let maxDate = toCalendar(setTime(plainDateTime, {hour: max}), new GregorianCalendar());
|
|
393
|
+
let maxAbsolute = [toAbsolute(maxDate, dateTime.timeZone, 'earlier'), toAbsolute(maxDate, dateTime.timeZone, 'later')]
|
|
394
|
+
.filter(ms => fromAbsolute(ms, dateTime.timeZone).day === maxDate.day).pop();
|
|
395
|
+
|
|
396
|
+
// Since hours may repeat, we need to operate on the absolute time in milliseconds.
|
|
397
|
+
// This is done in hours from the Unix epoch so that cycleValue works correctly,
|
|
398
|
+
// and then converted back to milliseconds.
|
|
399
|
+
let ms = epochFromDate(dateTime) - dateTime.offset;
|
|
400
|
+
let hours = Math.floor(ms / ONE_HOUR);
|
|
401
|
+
let remainder = ms % ONE_HOUR;
|
|
402
|
+
ms = cycleValue(
|
|
403
|
+
hours,
|
|
404
|
+
amount,
|
|
405
|
+
Math.floor(minAbsolute / ONE_HOUR),
|
|
406
|
+
Math.floor(maxAbsolute / ONE_HOUR),
|
|
407
|
+
options?.round
|
|
408
|
+
) * ONE_HOUR + remainder;
|
|
409
|
+
|
|
410
|
+
// Now compute the new timezone offset, and convert the absolute time back to local time.
|
|
411
|
+
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
|
|
412
|
+
}
|
|
413
|
+
case 'minute':
|
|
414
|
+
case 'second':
|
|
415
|
+
case 'millisecond':
|
|
416
|
+
// @ts-ignore
|
|
417
|
+
return cycleTime(dateTime, field, amount, options);
|
|
418
|
+
case 'era':
|
|
419
|
+
case 'year':
|
|
420
|
+
case 'month':
|
|
421
|
+
case 'day': {
|
|
422
|
+
let res = cycleDate(toCalendarDateTime(dateTime), field, amount, options);
|
|
423
|
+
let ms = toAbsolute(res, dateTime.timeZone);
|
|
424
|
+
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
|
|
425
|
+
}
|
|
426
|
+
default:
|
|
427
|
+
throw new Error('Unsupported field ' + field);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function setZoned(dateTime: ZonedDateTime, fields: DateFields & TimeFields, disambiguation?: Disambiguation): ZonedDateTime {
|
|
432
|
+
// Set the date/time fields, and recompute the UTC offset to account for DST changes.
|
|
433
|
+
// We also need to validate by converting back to a local time in case hours are skipped during forward DST transitions.
|
|
434
|
+
let plainDateTime = toCalendarDateTime(dateTime);
|
|
435
|
+
let res = setTime(set(plainDateTime, fields), fields);
|
|
436
|
+
|
|
437
|
+
// If the resulting plain date time values are equal, return the original time.
|
|
438
|
+
// We don't want to change the offset when setting the time to the same value.
|
|
439
|
+
if (res.compare(plainDateTime) === 0) {
|
|
440
|
+
return dateTime;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let ms = toAbsolute(res, dateTime.timeZone, disambiguation);
|
|
444
|
+
return toCalendar(fromAbsolute(ms, dateTime.timeZone), dateTime.calendar);
|
|
445
|
+
}
|
package/src/queries.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {AnyCalendarDate, AnyTime} from './types';
|
|
14
|
+
import {CalendarDate, CalendarDateTime, ZonedDateTime} from './CalendarDate';
|
|
15
|
+
import {fromAbsolute, toAbsolute, toCalendar, toCalendarDate} from './conversion';
|
|
16
|
+
import {weekStartData} from './weekStartData';
|
|
17
|
+
|
|
18
|
+
type DateValue = CalendarDate | CalendarDateTime | ZonedDateTime;
|
|
19
|
+
|
|
20
|
+
export function isSameDay(a: DateValue, b: DateValue): boolean {
|
|
21
|
+
b = toCalendar(b, a.calendar);
|
|
22
|
+
return a.era === b.era && a.year === b.year && a.month === b.month && a.day === b.day;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isSameMonth(a: DateValue, b: DateValue): boolean {
|
|
26
|
+
b = toCalendar(b, a.calendar);
|
|
27
|
+
// In the Japanese calendar, months can span multiple eras/years, so only compare the first of the month.
|
|
28
|
+
a = startOfMonth(a);
|
|
29
|
+
b = startOfMonth(b);
|
|
30
|
+
return a.era === b.era && a.year === b.year && a.month === b.month;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isSameYear(a: DateValue, b: DateValue): boolean {
|
|
34
|
+
b = toCalendar(b, a.calendar);
|
|
35
|
+
a = startOfYear(a);
|
|
36
|
+
b = startOfYear(b);
|
|
37
|
+
return a.era === b.era && a.year === b.year;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isEqualDay(a: DateValue, b: DateValue): boolean {
|
|
41
|
+
return a.calendar.identifier === b.calendar.identifier && a.era === b.era && a.year === b.year && a.month === b.month && a.day === b.day;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isEqualMonth(a: DateValue, b: DateValue): boolean {
|
|
45
|
+
a = startOfMonth(a);
|
|
46
|
+
b = startOfMonth(b);
|
|
47
|
+
return a.calendar.identifier === b.calendar.identifier && a.era === b.era && a.year === b.year && a.month === b.month;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isEqualYear(a: DateValue, b: DateValue): boolean {
|
|
51
|
+
a = startOfYear(a);
|
|
52
|
+
b = startOfYear(b);
|
|
53
|
+
return a.calendar.identifier === b.calendar.identifier && a.era === b.era && a.year === b.year;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isToday(date: DateValue, timeZone: string): boolean {
|
|
57
|
+
return isSameDay(date, today(timeZone));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getDayOfWeek(date: DateValue, locale: string) {
|
|
61
|
+
let julian = date.calendar.toJulianDay(date);
|
|
62
|
+
|
|
63
|
+
// If julian is negative, then julian % 7 will be negative, so we adjust
|
|
64
|
+
// accordingly. Julian day 0 is Monday.
|
|
65
|
+
let dayOfWeek = Math.ceil(julian + 1 - getWeekStart(locale)) % 7;
|
|
66
|
+
if (dayOfWeek < 0) {
|
|
67
|
+
dayOfWeek += 7;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return dayOfWeek;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function now(timeZone: string): ZonedDateTime {
|
|
74
|
+
return fromAbsolute(Date.now(), timeZone);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function today(timeZone: string): CalendarDate {
|
|
78
|
+
return toCalendarDate(now(timeZone));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function compareDate(a: AnyCalendarDate, b: AnyCalendarDate): number {
|
|
82
|
+
return a.calendar.toJulianDay(a) - b.calendar.toJulianDay(b);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function compareTime(a: AnyTime, b: AnyTime): number {
|
|
86
|
+
return timeToMs(a) - timeToMs(b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function timeToMs(a: AnyTime): number {
|
|
90
|
+
return a.hour * 60 * 60 * 1000 + a.minute * 60 * 1000 + a.second * 1000 + a.millisecond;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getHoursInDay(a: CalendarDate, timeZone: string): number {
|
|
94
|
+
let ms = toAbsolute(a, timeZone);
|
|
95
|
+
let tomorrow = a.add({days: 1});
|
|
96
|
+
let tomorrowMs = toAbsolute(tomorrow, timeZone);
|
|
97
|
+
return (tomorrowMs - ms) / 3600000;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let localTimeZone = null;
|
|
101
|
+
export function getLocalTimeZone(): string {
|
|
102
|
+
// TODO: invalidate this somehow?
|
|
103
|
+
if (localTimeZone == null) {
|
|
104
|
+
localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return localTimeZone;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
|
|
111
|
+
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;
|
|
112
|
+
export function startOfMonth(date: CalendarDate): CalendarDate;
|
|
113
|
+
export function startOfMonth(date: DateValue): DateValue;
|
|
114
|
+
export function startOfMonth(date: DateValue) {
|
|
115
|
+
// Use `subtract` instead of `set` so we don't get constrained in an era.
|
|
116
|
+
return date.subtract({days: date.day - 1});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function endOfMonth(date: ZonedDateTime): ZonedDateTime;
|
|
120
|
+
export function endOfMonth(date: CalendarDateTime): CalendarDateTime;
|
|
121
|
+
export function endOfMonth(date: CalendarDate): CalendarDate;
|
|
122
|
+
export function endOfMonth(date: DateValue): DateValue;
|
|
123
|
+
export function endOfMonth(date: DateValue) {
|
|
124
|
+
return date.add({days: date.calendar.getDaysInMonth(date) - date.day});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function startOfYear(date: ZonedDateTime): ZonedDateTime;
|
|
128
|
+
export function startOfYear(date: CalendarDateTime): CalendarDateTime;
|
|
129
|
+
export function startOfYear(date: CalendarDate): CalendarDate;
|
|
130
|
+
export function startOfYear(date: DateValue): DateValue;
|
|
131
|
+
export function startOfYear(date: DateValue) {
|
|
132
|
+
return startOfMonth(date.subtract({months: date.month - 1}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function endOfYear(date: ZonedDateTime): ZonedDateTime;
|
|
136
|
+
export function endOfYear(date: CalendarDateTime): CalendarDateTime;
|
|
137
|
+
export function endOfYear(date: CalendarDate): CalendarDate;
|
|
138
|
+
export function endOfYear(date: DateValue): DateValue;
|
|
139
|
+
export function endOfYear(date: DateValue) {
|
|
140
|
+
return endOfMonth(date.add({months: date.calendar.getMonthsInYear(date) - date.month}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getMinimumMonthInYear(date: AnyCalendarDate) {
|
|
144
|
+
if (date.calendar.getMinimumMonthInYear) {
|
|
145
|
+
return date.calendar.getMinimumMonthInYear(date);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getMinimumDayInMonth(date: AnyCalendarDate) {
|
|
152
|
+
if (date.calendar.getMinimumDayInMonth) {
|
|
153
|
+
return date.calendar.getMinimumDayInMonth(date);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function startOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
|
|
160
|
+
export function startOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
|
|
161
|
+
export function startOfWeek(date: CalendarDate, locale: string): CalendarDate;
|
|
162
|
+
export function startOfWeek(date: DateValue, locale: string): DateValue;
|
|
163
|
+
export function startOfWeek(date: DateValue, locale: string) {
|
|
164
|
+
let dayOfWeek = getDayOfWeek(date, locale);
|
|
165
|
+
return date.subtract({days: dayOfWeek});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function endOfWeek(date: ZonedDateTime, locale: string): ZonedDateTime;
|
|
169
|
+
export function endOfWeek(date: CalendarDateTime, locale: string): CalendarDateTime;
|
|
170
|
+
export function endOfWeek(date: CalendarDate, locale: string): CalendarDate;
|
|
171
|
+
export function endOfWeek(date: DateValue, locale: string) {
|
|
172
|
+
return startOfWeek(date, locale).add({days: 6});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const cachedRegions = new Map<string, string>();
|
|
176
|
+
|
|
177
|
+
function getRegion(locale: string) {
|
|
178
|
+
// If the Intl.Locale API is available, use it to get the region for the locale.
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
if (Intl.Locale) {
|
|
181
|
+
// Constructing an Intl.Locale is expensive, so cache the result.
|
|
182
|
+
let region = cachedRegions.get(locale);
|
|
183
|
+
if (!region) {
|
|
184
|
+
// @ts-ignore
|
|
185
|
+
region = new Intl.Locale(locale).maximize().region;
|
|
186
|
+
cachedRegions.set(locale, region);
|
|
187
|
+
}
|
|
188
|
+
return region;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// If not, just try splitting the string.
|
|
192
|
+
// If the second part of the locale string is 'u',
|
|
193
|
+
// then this is a unicode extension, so ignore it.
|
|
194
|
+
// Otherwise, it should be the region.
|
|
195
|
+
let part = locale.split('-')[1];
|
|
196
|
+
return part === 'u' ? null : part;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getWeekStart(locale: string) {
|
|
200
|
+
// TODO: use Intl.Locale for this once browsers support the weekInfo property
|
|
201
|
+
// https://github.com/tc39/proposal-intl-locale-info
|
|
202
|
+
let region = getRegion(locale);
|
|
203
|
+
return weekStartData[region] || 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function getWeeksInMonth(date: DateValue, locale: string) {
|
|
207
|
+
let days = date.calendar.getDaysInMonth(date);
|
|
208
|
+
return Math.ceil((getDayOfWeek(startOfMonth(date), locale) + days) / 7);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function minDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
|
|
212
|
+
return a.compare(b) <= 0 ? a : b;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function maxDate<A extends DateValue, B extends DateValue>(a: A, b: B): A | B {
|
|
216
|
+
return a.compare(b) >= 0 ? a : b;
|
|
217
|
+
}
|