@alexstukovnikov/oz-time 1.0.0 → 1.0.2

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.
@@ -0,0 +1,162 @@
1
+ import { OzTime } from '../core/core.js';
2
+
3
+ /**
4
+ * Модуль форматирования экземпляров {@link OzTime}.
5
+ *
6
+ * @module modules/format
7
+ */
8
+
9
+ /**
10
+ * Проверяет, является ли значение экземпляром OzTime.
11
+ *
12
+ * @private
13
+ * @param {*} value - Проверяемое значение.
14
+ * @throws {TypeError} Выбрасывается, если значение не является экземпляром OzTime.
15
+ * @returns {void}
16
+ */
17
+ function assertOzTime(value) {
18
+ if (!(value instanceof OzTime)) {
19
+ throw new TypeError('format: first argument must be OzTime');
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Дополняет значение ведущими нулями до нужной длины.
25
+ *
26
+ * @private
27
+ * @param {string|number} value - Исходное значение.
28
+ * @param {number} [length=2] - Итоговая длина строки.
29
+ * @returns {string} Строка, дополненная ведущими нулями.
30
+ */
31
+ function pad(value, length = 2) {
32
+ return String(value).padStart(length, '0');
33
+ }
34
+
35
+ /**
36
+ * Возвращает числовые части даты для форматирования.
37
+ *
38
+ * @private
39
+ * @param {OzTime} time - Экземпляр времени.
40
+ * @param {string} locale - Локаль форматирования.
41
+ * @returns {Object.<string, string>} Объект с числовыми частями даты и времени.
42
+ */
43
+ function getNumericParts(time, locale) {
44
+ const formatter = new Intl.DateTimeFormat(locale, {
45
+ timeZone: time.getTimezone(),
46
+ year: 'numeric',
47
+ month: 'numeric',
48
+ day: 'numeric',
49
+ hour: 'numeric',
50
+ minute: '2-digit',
51
+ second: '2-digit',
52
+ hour12: false,
53
+ });
54
+
55
+ const parts = formatter.formatToParts(new Date(time.getTimestamp()));
56
+ return Object.fromEntries(parts.map((part) => [part.type, part.value]));
57
+ }
58
+
59
+ /**
60
+ * Возвращает название месяца в нужном формате.
61
+ *
62
+ * @private
63
+ * @param {OzTime} time - Экземпляр времени.
64
+ * @param {string} locale - Локаль форматирования.
65
+ * @param {'long'|'short'|'narrow'} length - Длина названия месяца.
66
+ * @returns {string} Название месяца.
67
+ */
68
+ function getMonthName(time, locale, length) {
69
+ return new Intl.DateTimeFormat(locale, {
70
+ timeZone: time.getTimezone(),
71
+ month: length,
72
+ }).format(new Date(time.getTimestamp()));
73
+ }
74
+
75
+ /**
76
+ * Возвращает название дня недели в нужном формате.
77
+ *
78
+ * @private
79
+ * @param {OzTime} time - Экземпляр времени.
80
+ * @param {string} locale - Локаль форматирования.
81
+ * @param {'long'|'short'|'narrow'} length - Длина названия дня недели.
82
+ * @returns {string} Название дня недели.
83
+ */
84
+ function getWeekdayName(time, locale, length) {
85
+ return new Intl.DateTimeFormat(locale, {
86
+ timeZone: time.getTimezone(),
87
+ weekday: length,
88
+ }).format(new Date(time.getTimestamp()));
89
+ }
90
+
91
+ /**
92
+ * Возвращает строковое представление экземпляра {@link OzTime}
93
+ * по заданному шаблону с токенами.
94
+ *
95
+ * Поддерживаются токены `YYYY`, `YY`, `MMMM`, `MMM`, `MM`, `M`, `dddd`, `ddd`,
96
+ * `DD`, `D`, `HH`, `H`, `hh`, `h`, `mm`, `ss`, `SSS` и `A`.
97
+ *
98
+ * @param {OzTime} time - Экземпляр времени для форматирования.
99
+ * @param {string} template - Шаблон форматирования.
100
+ * @param {string} [locale] - Необязательное переопределение локали.
101
+ * @throws {TypeError} Выбрасывается, если первый аргумент не является экземпляром OzTime или template некорректен.
102
+ * @returns {string} Отформатированная строка.
103
+ * @example
104
+ * import { format, fromISO } from '@alexstukovnikov/oz-time';
105
+ *
106
+ * const time = fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
107
+ * console.log(format(time, 'DD.MM.YYYY HH:mm')); // ожидаемый результат: 25.05.2024 12:00
108
+ */
109
+ export function format(time, template, locale) {
110
+ assertOzTime(time);
111
+
112
+ if (typeof template !== 'string' || template.trim() === '') {
113
+ throw new TypeError('format: template must be a non-empty string');
114
+ }
115
+
116
+ const usedLocale = locale ?? time.getLocale();
117
+ const parts = getNumericParts(time, usedLocale);
118
+
119
+ const year = Number(parts.year);
120
+ const month = Number(parts.month);
121
+ const day = Number(parts.day);
122
+ const hour24 = Number(parts.hour);
123
+ const minute = Number(parts.minute);
124
+ const second = Number(parts.second);
125
+ const millisecond = new Date(time.getTimestamp()).getUTCMilliseconds();
126
+
127
+ const hour12base = hour24 % 12;
128
+ const hour12 = hour12base === 0 ? 12 : hour12base;
129
+ const meridiem = hour24 >= 12 ? 'PM' : 'AM';
130
+
131
+ const tokens = {
132
+ YYYY: String(year),
133
+ YY: String(year).slice(-2),
134
+
135
+ MMMM: getMonthName(time, usedLocale, 'long'),
136
+ MMM: getMonthName(time, usedLocale, 'short'),
137
+ MM: pad(month),
138
+ M: String(month),
139
+
140
+ dddd: getWeekdayName(time, usedLocale, 'long'),
141
+ ddd: getWeekdayName(time, usedLocale, 'short'),
142
+
143
+ DD: pad(day),
144
+ D: String(day),
145
+
146
+ HH: pad(hour24),
147
+ H: String(hour24),
148
+
149
+ hh: pad(hour12),
150
+ h: String(hour12),
151
+
152
+ mm: pad(minute),
153
+ ss: pad(second),
154
+ SSS: pad(millisecond, 3),
155
+
156
+ A: meridiem,
157
+ };
158
+
159
+ const tokenPattern = /YYYY|MMMM|MMM|MM|M|dddd|ddd|DD|D|HH|H|hh|h|mm|ss|SSS|YY|A/g;
160
+
161
+ return template.replace(tokenPattern, (token) => tokens[token] ?? token);
162
+ }
@@ -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
+ }