@alexstukovnikov/oz-time 1.0.0 → 1.0.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 +45 -48
- package/src/core/core.js +353 -0
- package/src/core/factory.js +194 -0
- package/src/index.js +22 -0
- package/src/modules/arithmetic.js +100 -0
- package/src/modules/compare.js +197 -0
- package/src/modules/duration.js +165 -0
- package/src/modules/format.js +162 -0
- package/src/modules/interval.js +190 -0
- package/src/modules/timezone.js +112 -0
- package/src/utils/calendar.js +277 -0
- package/src/utils/units.js +135 -0
- package/dist/oz-time.cjs +0 -1
- package/dist/oz-time.esm.js +0 -426
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { OzTime } from '../core/core.js';
|
|
2
|
+
import { normalizeUnit, isFixedUnit, unitToMilliseconds } from '../utils/units.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Модуль интервалов времени.
|
|
6
|
+
*
|
|
7
|
+
* @module modules/interval
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Проверяет, является ли значение экземпляром OzTime.
|
|
12
|
+
*
|
|
13
|
+
* @private
|
|
14
|
+
* @param {*} value - Проверяемое значение.
|
|
15
|
+
* @param {string} name - Имя параметра.
|
|
16
|
+
* @throws {TypeError} Выбрасывается, если значение не является экземпляром OzTime.
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
function assertOzTime(value, name) {
|
|
20
|
+
if (!(value instanceof OzTime)) {
|
|
21
|
+
throw new TypeError(`${name} must be OzTime`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Представляет замкнутый интервал между двумя значениями времени.
|
|
27
|
+
*
|
|
28
|
+
* @class
|
|
29
|
+
* @example
|
|
30
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
31
|
+
*
|
|
32
|
+
* const start = fromISO('2024-05-25T10:00:00Z');
|
|
33
|
+
* const end = fromISO('2024-05-25T12:00:00Z');
|
|
34
|
+
* const range = new Interval(start, end);
|
|
35
|
+
* console.log(range.contains(fromISO('2024-05-25T11:00:00Z'))); // ожидаемый результат: true
|
|
36
|
+
*/
|
|
37
|
+
export class Interval {
|
|
38
|
+
/**
|
|
39
|
+
* Создаёт новый экземпляр Interval.
|
|
40
|
+
*
|
|
41
|
+
* @param {OzTime} start - Начало интервала.
|
|
42
|
+
* @param {OzTime} end - Конец интервала.
|
|
43
|
+
* @throws {TypeError} Выбрасывается, если start или end не являются экземплярами OzTime.
|
|
44
|
+
* @throws {RangeError} Выбрасывается, если start больше end.
|
|
45
|
+
*/
|
|
46
|
+
constructor(start, end) {
|
|
47
|
+
assertOzTime(start, 'start');
|
|
48
|
+
assertOzTime(end, 'end');
|
|
49
|
+
|
|
50
|
+
if (start.getTimestamp() > end.getTimestamp()) {
|
|
51
|
+
throw new RangeError('Interval: start must be before or equal to end');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this._start = start;
|
|
55
|
+
this._end = end;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Возвращает начало интервала.
|
|
60
|
+
*
|
|
61
|
+
* @returns {OzTime} Начальная граница интервала.
|
|
62
|
+
* @example
|
|
63
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
64
|
+
*
|
|
65
|
+
* const range = new Interval(
|
|
66
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
67
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
68
|
+
* );
|
|
69
|
+
* console.log(range.getStart().toISOString()); // ожидаемый результат: 2024-05-25T10:00:00.000Z
|
|
70
|
+
*/
|
|
71
|
+
getStart() {
|
|
72
|
+
return this._start;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Возвращает конец интервала.
|
|
77
|
+
*
|
|
78
|
+
* @returns {OzTime} Конечная граница интервала.
|
|
79
|
+
* @example
|
|
80
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
81
|
+
*
|
|
82
|
+
* const range = new Interval(
|
|
83
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
84
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
85
|
+
* );
|
|
86
|
+
* console.log(range.getEnd().toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
|
|
87
|
+
*/
|
|
88
|
+
getEnd() {
|
|
89
|
+
return this._end;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Проверяет, содержит ли интервал переданное значение времени.
|
|
94
|
+
*
|
|
95
|
+
* @param {OzTime} moment - Проверяемое значение.
|
|
96
|
+
* @throws {TypeError} Выбрасывается, если moment не является экземпляром OzTime.
|
|
97
|
+
* @returns {boolean} `true`, если значение входит в интервал.
|
|
98
|
+
* @example
|
|
99
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
100
|
+
*
|
|
101
|
+
* const range = new Interval(
|
|
102
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
103
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
104
|
+
* );
|
|
105
|
+
* console.log(range.contains(fromISO('2024-05-25T11:00:00Z'))); // ожидаемый результат: true
|
|
106
|
+
*/
|
|
107
|
+
contains(moment) {
|
|
108
|
+
assertOzTime(moment, 'moment');
|
|
109
|
+
|
|
110
|
+
const ts = moment.getTimestamp();
|
|
111
|
+
return ts >= this._start.getTimestamp() && ts <= this._end.getTimestamp();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Проверяет, пересекается ли текущий интервал с другим интервалом.
|
|
116
|
+
*
|
|
117
|
+
* @param {Interval} other - Второй интервал.
|
|
118
|
+
* @throws {TypeError} Выбрасывается, если other не является экземпляром Interval.
|
|
119
|
+
* @returns {boolean} `true`, если интервалы пересекаются.
|
|
120
|
+
* @example
|
|
121
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
122
|
+
*
|
|
123
|
+
* const a = new Interval(
|
|
124
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
125
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
126
|
+
* );
|
|
127
|
+
* const b = new Interval(
|
|
128
|
+
* fromISO('2024-05-25T11:00:00Z'),
|
|
129
|
+
* fromISO('2024-05-25T13:00:00Z')
|
|
130
|
+
* );
|
|
131
|
+
* console.log(a.overlaps(b)); // ожидаемый результат: true
|
|
132
|
+
*/
|
|
133
|
+
overlaps(other) {
|
|
134
|
+
if (!(other instanceof Interval)) {
|
|
135
|
+
throw new TypeError('other must be Interval');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const startA = this._start.getTimestamp();
|
|
139
|
+
const endA = this._end.getTimestamp();
|
|
140
|
+
const startB = other.getStart().getTimestamp();
|
|
141
|
+
const endB = other.getEnd().getTimestamp();
|
|
142
|
+
|
|
143
|
+
return startA <= endB && startB <= endA;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Возвращает длительность интервала в фиксированной единице времени.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} [unit='millisecond'] - Фиксированная единица времени.
|
|
150
|
+
* @throws {Error} Выбрасывается, если unit не является фиксированной единицей времени.
|
|
151
|
+
* @returns {number} Длительность интервала в указанной единице.
|
|
152
|
+
* @example
|
|
153
|
+
* import { Interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
154
|
+
*
|
|
155
|
+
* const range = new Interval(
|
|
156
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
157
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
158
|
+
* );
|
|
159
|
+
* console.log(range.duration('hour')); // ожидаемый результат: 2
|
|
160
|
+
*/
|
|
161
|
+
duration(unit = 'millisecond') {
|
|
162
|
+
const normalizedUnit = normalizeUnit(unit);
|
|
163
|
+
|
|
164
|
+
if (!isFixedUnit(normalizedUnit)) {
|
|
165
|
+
throw new Error(`Interval.duration supports only fixed units: ${unit}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const diffMs = this._end.getTimestamp() - this._start.getTimestamp();
|
|
169
|
+
return diffMs / unitToMilliseconds(normalizedUnit);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Создаёт и возвращает экземпляр {@link Interval}.
|
|
175
|
+
*
|
|
176
|
+
* @param {OzTime} start - Начало интервала.
|
|
177
|
+
* @param {OzTime} end - Конец интервала.
|
|
178
|
+
* @returns {Interval} Новый экземпляр Interval.
|
|
179
|
+
* @example
|
|
180
|
+
* import { interval, fromISO } from '@alexstukovnikov/oz-time';
|
|
181
|
+
*
|
|
182
|
+
* const range = interval(
|
|
183
|
+
* fromISO('2024-05-25T10:00:00Z'),
|
|
184
|
+
* fromISO('2024-05-25T12:00:00Z')
|
|
185
|
+
* );
|
|
186
|
+
* console.log(range.duration('hour')); // ожидаемый результат: 2
|
|
187
|
+
*/
|
|
188
|
+
export function interval(start, end) {
|
|
189
|
+
return new Interval(start, end);
|
|
190
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { OzTime } from '../core/core.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Модуль для работы с часовыми поясами.
|
|
5
|
+
*
|
|
6
|
+
* @module modules/timezone
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Проверяет корректность идентификатора часового пояса.
|
|
11
|
+
*
|
|
12
|
+
* @private
|
|
13
|
+
* @param {string} timezone - Идентификатор часового пояса в формате IANA.
|
|
14
|
+
* @throws {TypeError} Выбрасывается, если timezone пустой или не является строкой.
|
|
15
|
+
* @throws {Error} Выбрасывается, если timezone не поддерживается.
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function validateTimezone(timezone) {
|
|
19
|
+
if (typeof timezone !== 'string' || timezone.trim() === '') {
|
|
20
|
+
throw new TypeError('setTimezone: timezone must be a non-empty string');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!Intl.supportedValuesOf('timeZone').includes(timezone)) {
|
|
24
|
+
throw new Error(`Unsupported timezone: ${timezone}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Вычисляет смещение часового пояса относительно UTC для конкретного timestamp.
|
|
30
|
+
*
|
|
31
|
+
* @private
|
|
32
|
+
* @param {number} timestamp - Unix timestamp в миллисекундах.
|
|
33
|
+
* @param {string} timeZone - Часовой пояс в формате IANA.
|
|
34
|
+
* @returns {number} Смещение в минутах относительно UTC.
|
|
35
|
+
*/
|
|
36
|
+
function getOffsetMinutesFor(timestamp, timeZone) {
|
|
37
|
+
const date = new Date(timestamp);
|
|
38
|
+
|
|
39
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
40
|
+
timeZone,
|
|
41
|
+
year: 'numeric',
|
|
42
|
+
month: '2-digit',
|
|
43
|
+
day: '2-digit',
|
|
44
|
+
hour: '2-digit',
|
|
45
|
+
minute: '2-digit',
|
|
46
|
+
second: '2-digit',
|
|
47
|
+
hour12: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const parts = formatter.formatToParts(date);
|
|
51
|
+
const lookup = Object.fromEntries(parts.map((p) => [p.type, p.value]));
|
|
52
|
+
|
|
53
|
+
const year = Number(lookup.year);
|
|
54
|
+
const month = Number(lookup.month);
|
|
55
|
+
const day = Number(lookup.day);
|
|
56
|
+
const hour = Number(lookup.hour);
|
|
57
|
+
const minute = Number(lookup.minute);
|
|
58
|
+
const second = Number(lookup.second);
|
|
59
|
+
|
|
60
|
+
const utcTimestamp = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
61
|
+
|
|
62
|
+
return (utcTimestamp - timestamp) / 60000;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Возвращает новый экземпляр {@link OzTime} с тем же timestamp и locale,
|
|
67
|
+
* но с другим часовым поясом.
|
|
68
|
+
*
|
|
69
|
+
* Абсолютный момент времени при этом не изменяется.
|
|
70
|
+
*
|
|
71
|
+
* @param {OzTime} time - Исходный экземпляр {@link OzTime}.
|
|
72
|
+
* @param {string} timezone - Новый часовой пояс в формате IANA.
|
|
73
|
+
* @throws {TypeError} Выбрасывается, если первый аргумент не является экземпляром OzTime или timezone некорректен.
|
|
74
|
+
* @throws {Error} Выбрасывается, если timezone не поддерживается.
|
|
75
|
+
* @returns {OzTime} Новый экземпляр OzTime с другим часовым поясом.
|
|
76
|
+
* @example
|
|
77
|
+
* import { setTimezone, fromISO } from '@alexstukovnikov/oz-time';
|
|
78
|
+
*
|
|
79
|
+
* const time = fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
|
|
80
|
+
* const moscow = setTimezone(time, 'Europe/Moscow');
|
|
81
|
+
* console.log(moscow.getTimezone()); // ожидаемый результат: Europe/Moscow
|
|
82
|
+
*/
|
|
83
|
+
export function setTimezone(time, timezone) {
|
|
84
|
+
if (!(time instanceof OzTime)) {
|
|
85
|
+
throw new TypeError('tz: first argument must be OzTime');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
validateTimezone(timezone);
|
|
89
|
+
|
|
90
|
+
return new OzTime(time.getTimestamp(), timezone, time.getLocale());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Возвращает смещение часового пояса экземпляра относительно UTC в минутах.
|
|
95
|
+
*
|
|
96
|
+
* @param {OzTime} time - Экземпляр времени.
|
|
97
|
+
* @throws {TypeError} Выбрасывается, если аргумент не является экземпляром OzTime.
|
|
98
|
+
* @returns {number} Смещение в минутах относительно UTC.
|
|
99
|
+
* @example
|
|
100
|
+
* import { getTimezoneOffset, fromISO } from '@alexstukovnikov/oz-time';
|
|
101
|
+
*
|
|
102
|
+
* const time = fromISO('2024-05-25T12:00:00Z', 'Europe/Moscow', 'ru-RU');
|
|
103
|
+
* console.log(getTimezoneOffset(time)); // ожидаемый результат: 180
|
|
104
|
+
*/
|
|
105
|
+
export function getTimezoneOffset(time) {
|
|
106
|
+
if (!(time instanceof OzTime)) {
|
|
107
|
+
throw new TypeError('getTimezoneOffset: argument must be OzTime');
|
|
108
|
+
}
|
|
109
|
+
const timestamp = time.getTimestamp();
|
|
110
|
+
const timeZone = time.getTimezone();
|
|
111
|
+
return getOffsetMinutesFor(timestamp, timeZone);
|
|
112
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { normalizeUnit, isFixedUnit, unitToMilliseconds } from './units.js';
|
|
2
|
+
import { OzTime } from '../core/core.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Календарные утилиты для работы с датами, високосными годами
|
|
6
|
+
* и разницей между значениями времени.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/calendar
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Проверяет корректность timestamp.
|
|
13
|
+
*
|
|
14
|
+
* @private
|
|
15
|
+
* @param {number} timestamp - Unix timestamp в миллисекундах.
|
|
16
|
+
* @param {string} name - Имя вызывающей функции.
|
|
17
|
+
* @throws {TypeError} Выбрасывается, если timestamp некорректен.
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
function assertValidTimestamp(timestamp, name) {
|
|
21
|
+
if (typeof timestamp !== 'number' || Number.isNaN(timestamp)) {
|
|
22
|
+
throw new TypeError(`${name}: timestamp must be a valid number`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Проверяет корректность количества единиц времени.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
* @param {number} amount - Количество единиц времени.
|
|
31
|
+
* @param {string} name - Имя вызывающей функции.
|
|
32
|
+
* @throws {TypeError} Выбрасывается, если amount некорректен.
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
function assertValidAmount(amount, name) {
|
|
36
|
+
if (typeof amount !== 'number' || Number.isNaN(amount)) {
|
|
37
|
+
throw new TypeError(`${name}: amount must be a valid number`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Проверяет, является ли значение экземпляром OzTime.
|
|
43
|
+
*
|
|
44
|
+
* @private
|
|
45
|
+
* @param {*} value - Проверяемое значение.
|
|
46
|
+
* @param {string} name - Имя параметра.
|
|
47
|
+
* @throws {TypeError} Выбрасывается, если значение не является экземпляром OzTime.
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
50
|
+
function assertOzTime(value, name) {
|
|
51
|
+
if (!(value instanceof OzTime)) {
|
|
52
|
+
throw new TypeError(`${name} must be OzTime`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Вычисляет календарную разницу в месяцах между двумя timestamp.
|
|
58
|
+
*
|
|
59
|
+
* @private
|
|
60
|
+
* @param {number} leftTimestamp - Левый timestamp.
|
|
61
|
+
* @param {number} rightTimestamp - Правый timestamp.
|
|
62
|
+
* @returns {number} Разница в месяцах.
|
|
63
|
+
*/
|
|
64
|
+
function diffInMonths(leftTimestamp, rightTimestamp) {
|
|
65
|
+
const left = new Date(leftTimestamp);
|
|
66
|
+
const right = new Date(rightTimestamp);
|
|
67
|
+
|
|
68
|
+
let months = (left.getUTCFullYear() - right.getUTCFullYear()) * 12 + (left.getUTCMonth() - right.getUTCMonth());
|
|
69
|
+
|
|
70
|
+
const leftDay = left.getUTCDate();
|
|
71
|
+
const rightDay = right.getUTCDate();
|
|
72
|
+
|
|
73
|
+
if (months > 0 && leftDay < rightDay) {
|
|
74
|
+
months -= 1;
|
|
75
|
+
} else if (months < 0 && leftDay > rightDay) {
|
|
76
|
+
months += 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return months;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Вычисляет календарную разницу в годах между двумя timestamp.
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
* @param {number} leftTimestamp - Левый timestamp.
|
|
87
|
+
* @param {number} rightTimestamp - Правый timestamp.
|
|
88
|
+
* @returns {number} Разница в годах.
|
|
89
|
+
*/
|
|
90
|
+
function diffInYears(leftTimestamp, rightTimestamp) {
|
|
91
|
+
const left = new Date(leftTimestamp);
|
|
92
|
+
const right = new Date(rightTimestamp);
|
|
93
|
+
|
|
94
|
+
let years = left.getUTCFullYear() - right.getUTCFullYear();
|
|
95
|
+
|
|
96
|
+
const leftMonth = left.getUTCMonth();
|
|
97
|
+
const rightMonth = right.getUTCMonth();
|
|
98
|
+
const leftDay = left.getUTCDate();
|
|
99
|
+
const rightDay = right.getUTCDate();
|
|
100
|
+
|
|
101
|
+
if (years > 0 && (leftMonth < rightMonth || (leftMonth === rightMonth && leftDay < rightDay))) {
|
|
102
|
+
years -= 1;
|
|
103
|
+
} else if (years < 0 && (leftMonth > rightMonth || (leftMonth === rightMonth && leftDay > rightDay))) {
|
|
104
|
+
years += 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return years;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Проверяет, является ли год високосным по григорианскому календарю.
|
|
112
|
+
*
|
|
113
|
+
* @param {number} year - Год.
|
|
114
|
+
* @throws {TypeError} Выбрасывается, если year не является целым числом.
|
|
115
|
+
* @returns {boolean} `true`, если год високосный.
|
|
116
|
+
* @example
|
|
117
|
+
* import { isLeapYear } from '@alexstukovnikov/oz-time';
|
|
118
|
+
*
|
|
119
|
+
* console.log(isLeapYear(2024)); // ожидаемый результат: true
|
|
120
|
+
*/
|
|
121
|
+
export function isLeapYear(year) {
|
|
122
|
+
if (!Number.isInteger(year)) {
|
|
123
|
+
throw new TypeError('isLeapYear: year must be an integer');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Возвращает количество дней в указанном месяце указанного года.
|
|
131
|
+
*
|
|
132
|
+
* @param {number} year - Год.
|
|
133
|
+
* @param {number} month - Месяц от 1 до 12.
|
|
134
|
+
* @throws {TypeError} Выбрасывается, если year или month не являются целыми числами.
|
|
135
|
+
* @throws {RangeError} Выбрасывается, если month вне диапазона от 1 до 12.
|
|
136
|
+
* @returns {number} Количество дней в месяце.
|
|
137
|
+
* @example
|
|
138
|
+
* import { daysInMonth } from '@alexstukovnikov/oz-time';
|
|
139
|
+
*
|
|
140
|
+
* console.log(daysInMonth(2024, 2)); // ожидаемый результат: 29
|
|
141
|
+
*/
|
|
142
|
+
export function daysInMonth(year, month) {
|
|
143
|
+
if (!Number.isInteger(year) || !Number.isInteger(month)) {
|
|
144
|
+
throw new TypeError('daysInMonth: year and month must be integers');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (month < 1 || month > 12) {
|
|
148
|
+
throw new RangeError('daysInMonth: month must be between 1 and 12');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Возвращает новый timestamp, увеличенный на указанное количество
|
|
156
|
+
* фиксированных единиц времени.
|
|
157
|
+
*
|
|
158
|
+
* @param {number} timestamp - Unix timestamp в миллисекундах.
|
|
159
|
+
* @param {number} amount - Количество единиц времени.
|
|
160
|
+
* @param {string} unit - Фиксированная единица времени.
|
|
161
|
+
* @throws {TypeError} Выбрасывается, если timestamp или amount некорректны.
|
|
162
|
+
* @throws {Error} Выбрасывается, если unit не является фиксированной единицей.
|
|
163
|
+
* @returns {number} Новый Unix timestamp в миллисекундах.
|
|
164
|
+
* @example
|
|
165
|
+
* import { addByFixedUnit } from './utils/calendar.js';
|
|
166
|
+
*
|
|
167
|
+
* console.log(addByFixedUnit(1716638400000, 1, 'day')); // ожидаемый результат: 1716724800000
|
|
168
|
+
*/
|
|
169
|
+
export function addByFixedUnit(timestamp, amount, unit) {
|
|
170
|
+
assertValidTimestamp(timestamp, 'addByFixedUnit');
|
|
171
|
+
assertValidAmount(amount, 'addByFixedUnit');
|
|
172
|
+
|
|
173
|
+
const normalizedUnit = normalizeUnit(unit);
|
|
174
|
+
|
|
175
|
+
if (!isFixedUnit(normalizedUnit)) {
|
|
176
|
+
throw new Error(`addByFixedUnit does not support calendar unit: ${unit}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return timestamp + amount * unitToMilliseconds(normalizedUnit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Возвращает новый timestamp, увеличенный на указанное количество
|
|
184
|
+
* календарных единиц времени.
|
|
185
|
+
*
|
|
186
|
+
* Поддерживаются только `month` и `year`.
|
|
187
|
+
*
|
|
188
|
+
* @param {number} timestamp - Unix timestamp в миллисекундах.
|
|
189
|
+
* @param {number} amount - Количество единиц времени.
|
|
190
|
+
* @param {string} unit - Календарная единица времени.
|
|
191
|
+
* @throws {TypeError} Выбрасывается, если timestamp или amount некорректны.
|
|
192
|
+
* @throws {Error} Выбрасывается, если unit не поддерживается.
|
|
193
|
+
* @returns {number} Новый Unix timestamp в миллисекундах.
|
|
194
|
+
* @example
|
|
195
|
+
* import { addByCalendarUnit } from './utils/calendar.js';
|
|
196
|
+
*
|
|
197
|
+
* console.log(addByCalendarUnit(Date.UTC(2024, 0, 31, 0, 0, 0, 0), 1, 'month')); // ожидаемый результат: 1709164800000
|
|
198
|
+
*/
|
|
199
|
+
export function addByCalendarUnit(timestamp, amount, unit) {
|
|
200
|
+
assertValidTimestamp(timestamp, 'addByCalendarUnit');
|
|
201
|
+
assertValidAmount(amount, 'addByCalendarUnit');
|
|
202
|
+
|
|
203
|
+
const normalizedUnit = normalizeUnit(unit);
|
|
204
|
+
const date = new Date(timestamp);
|
|
205
|
+
|
|
206
|
+
if (normalizedUnit === 'month') {
|
|
207
|
+
const originalDay = date.getUTCDate();
|
|
208
|
+
|
|
209
|
+
date.setUTCDate(1);
|
|
210
|
+
date.setUTCMonth(date.getUTCMonth() + amount);
|
|
211
|
+
|
|
212
|
+
const maxDay = daysInMonth(date.getUTCFullYear(), date.getUTCMonth() + 1);
|
|
213
|
+
date.setUTCDate(Math.min(originalDay, maxDay));
|
|
214
|
+
|
|
215
|
+
return date.getTime();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (normalizedUnit === 'year') {
|
|
219
|
+
const originalMonth = date.getUTCMonth();
|
|
220
|
+
const originalDay = date.getUTCDate();
|
|
221
|
+
|
|
222
|
+
date.setUTCDate(1);
|
|
223
|
+
date.setUTCFullYear(date.getUTCFullYear() + amount);
|
|
224
|
+
date.setUTCMonth(originalMonth);
|
|
225
|
+
|
|
226
|
+
const maxDay = daysInMonth(date.getUTCFullYear(), originalMonth + 1);
|
|
227
|
+
date.setUTCDate(Math.min(originalDay, maxDay));
|
|
228
|
+
|
|
229
|
+
return date.getTime();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new Error(`addByCalendarUnit supports only month and year: ${unit}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Возвращает числовую разницу между двумя экземплярами {@link OzTime}
|
|
237
|
+
* в указанной единице времени.
|
|
238
|
+
*
|
|
239
|
+
* Для фиксированных единиц может возвращать дробное число,
|
|
240
|
+
* для месяцев и лет возвращает целочисленную календарную разницу.
|
|
241
|
+
*
|
|
242
|
+
* @param {OzTime} left - Левое значение.
|
|
243
|
+
* @param {OzTime} right - Правое значение.
|
|
244
|
+
* @param {string} [unit='millisecond'] - Единица времени.
|
|
245
|
+
* @throws {TypeError} Выбрасывается, если хотя бы один аргумент не является экземпляром OzTime.
|
|
246
|
+
* @throws {Error} Выбрасывается, если unit не поддерживается.
|
|
247
|
+
* @returns {number} Разница между двумя значениями времени.
|
|
248
|
+
* @example
|
|
249
|
+
* import { diff } from './utils/calendar.js';
|
|
250
|
+
* import { fromISO } from '@alexstukovnikov/oz-time';
|
|
251
|
+
*
|
|
252
|
+
* const a = fromISO('2024-05-25T14:00:00Z');
|
|
253
|
+
* const b = fromISO('2024-05-25T12:00:00Z');
|
|
254
|
+
* console.log(diff(a, b, 'hour')); // ожидаемый результат: 2
|
|
255
|
+
*/
|
|
256
|
+
export function diff(left, right, unit = 'millisecond') {
|
|
257
|
+
assertOzTime(left, 'left');
|
|
258
|
+
assertOzTime(right, 'right');
|
|
259
|
+
|
|
260
|
+
const normalizedUnit = normalizeUnit(unit);
|
|
261
|
+
const leftTimestamp = left.getTimestamp();
|
|
262
|
+
const rightTimestamp = right.getTimestamp();
|
|
263
|
+
|
|
264
|
+
if (isFixedUnit(normalizedUnit)) {
|
|
265
|
+
return (leftTimestamp - rightTimestamp) / unitToMilliseconds(normalizedUnit);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (normalizedUnit === 'month') {
|
|
269
|
+
return diffInMonths(leftTimestamp, rightTimestamp);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (normalizedUnit === 'year') {
|
|
273
|
+
return diffInYears(leftTimestamp, rightTimestamp);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new Error(`Unsupported unit: ${unit}`);
|
|
277
|
+
}
|