@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.
@@ -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
+ }