@escapenavigator/utils 1.10.106 → 1.10.108
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/dist/date-timezone.d.ts +5 -0
- package/dist/date-timezone.js +190 -0
- package/dist/date.d.ts +53 -0
- package/dist/date.js +362 -0
- package/dist/date.test.d.ts +1 -0
- package/dist/date.test.js +61 -0
- package/dist/format-amount/index.js +12 -12
- package/dist/get-documents-links.js +1 -2
- package/dist/get-email-result-section.js +1 -2
- package/dist/get-email-upsales-section.js +1 -2
- package/dist/get-full-name.js +1 -1
- package/dist/get-handled-error-message.js +1 -1
- package/dist/get-order-cancelation-date/index.js +4 -10
- package/dist/get-order-cancelation-state/index.js +12 -11
- package/dist/get-order-cancelation-state/index.test.js +5 -5
- package/dist/get-players-price.js +1 -1
- package/dist/get-promocode-discount.js +2 -0
- package/dist/get-service-error.js +3 -3
- package/dist/get-slot-cancelation-date/index.d.ts +3 -1
- package/dist/get-slot-cancelation-date/index.js +11 -14
- package/dist/get-slot-cancelation-date/index.test.js +4 -8
- package/dist/get-static-color.js +1 -2
- package/dist/has-text-outside-tags.js +1 -2
- package/dist/index.d.ts +9 -3
- package/dist/index.js +9 -3
- package/dist/is-axios-error.js +1 -2
- package/dist/is-network-error.js +1 -2
- package/dist/pick/index.d.ts +2 -0
- package/dist/pick/index.js +16 -0
- package/dist/pick/index.test.d.ts +1 -0
- package/dist/pick/index.test.js +23 -0
- package/dist/promocode-error-codes.d.ts +72 -0
- package/dist/promocode-error-codes.js +61 -0
- package/dist/promocode-nominal-rules.d.ts +29 -0
- package/dist/promocode-nominal-rules.js +39 -0
- package/dist/promocode-nominal-rules.spec.d.ts +1 -0
- package/dist/promocode-nominal-rules.spec.js +74 -0
- package/dist/serialize-slot.d.ts +1 -20
- package/dist/serialize-slot.js +8 -78
- package/dist/transform-text.js +6 -4
- package/dist/tz-date.d.ts +1 -1
- package/dist/tz-date.js +8 -9
- package/dist/utm-touchpoints.d.ts +63 -0
- package/dist/utm-touchpoints.js +66 -0
- package/dist/utm-touchpoints.spec.d.ts +1 -0
- package/dist/utm-touchpoints.spec.js +95 -0
- package/dist/validate-by-dto.d.ts +1 -1
- package/dist/validate-by-dto.js +2 -3
- package/dist/validate-promocode.d.ts +9 -4
- package/dist/validate-promocode.js +57 -38
- package/dist/validate-promocode.spec.d.ts +1 -0
- package/dist/validate-promocode.spec.js +129 -0
- package/package.json +4 -25
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utcToZonedTime / zonedTimeToUtc (семантика date-fns-tz@2, Intl для IANA).
|
|
3
|
+
*/
|
|
4
|
+
export declare function utcToZonedTime(dirtyDate: Date | string | number, timeZone: string): Date;
|
|
5
|
+
export declare function zonedTimeToUtc(dateInput: Date | string | number, timeZone: string): Date;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* utcToZonedTime / zonedTimeToUtc (семантика date-fns-tz@2, Intl для IANA).
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.utcToZonedTime = utcToZonedTime;
|
|
7
|
+
exports.zonedTimeToUtc = zonedTimeToUtc;
|
|
8
|
+
const MS_HOUR = 3600000;
|
|
9
|
+
const MS_MINUTE = 60000;
|
|
10
|
+
function isValidTimezoneIANA(timeZoneString) {
|
|
11
|
+
try {
|
|
12
|
+
Intl.DateTimeFormat(undefined, { timeZone: timeZoneString });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const dtfCache = {};
|
|
20
|
+
function getDateTimeFormat(timeZone) {
|
|
21
|
+
if (!dtfCache[timeZone]) {
|
|
22
|
+
const base = {
|
|
23
|
+
timeZone,
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
month: 'numeric',
|
|
26
|
+
day: '2-digit',
|
|
27
|
+
hour: '2-digit',
|
|
28
|
+
minute: '2-digit',
|
|
29
|
+
second: '2-digit',
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
dtfCache[timeZone] = new Intl.DateTimeFormat('en-US', {
|
|
33
|
+
...base,
|
|
34
|
+
hourCycle: 'h23',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
dtfCache[timeZone] = new Intl.DateTimeFormat('en-US', {
|
|
39
|
+
...base,
|
|
40
|
+
hour12: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return dtfCache[timeZone];
|
|
45
|
+
}
|
|
46
|
+
function tzTokenizeDate(date, timeZone) {
|
|
47
|
+
const dtf = getDateTimeFormat(timeZone);
|
|
48
|
+
const formatted = dtf.formatToParts(date);
|
|
49
|
+
const typeToPos = {
|
|
50
|
+
year: 0,
|
|
51
|
+
month: 1,
|
|
52
|
+
day: 2,
|
|
53
|
+
hour: 3,
|
|
54
|
+
minute: 4,
|
|
55
|
+
second: 5,
|
|
56
|
+
};
|
|
57
|
+
const filled = [NaN, NaN, NaN, NaN, NaN, NaN];
|
|
58
|
+
for (let k = 0; k < formatted.length; k += 1) {
|
|
59
|
+
const p = formatted[k];
|
|
60
|
+
const pos = typeToPos[p.type];
|
|
61
|
+
if (pos !== undefined && pos >= 0) {
|
|
62
|
+
filled[pos] = parseInt(p.value, 10);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return filled;
|
|
66
|
+
}
|
|
67
|
+
function utcWallMs(fullYear, monthIndex, day, hours, minutes, seconds, milliseconds) {
|
|
68
|
+
return Date.UTC(fullYear, monthIndex, day, hours, minutes, seconds, milliseconds);
|
|
69
|
+
}
|
|
70
|
+
function wallCalendarPartsFromString(s) {
|
|
71
|
+
const t = s.trim();
|
|
72
|
+
let m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?/.exec(t);
|
|
73
|
+
if (!m && t.includes('T')) {
|
|
74
|
+
m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?/.exec(t);
|
|
75
|
+
}
|
|
76
|
+
if (!m)
|
|
77
|
+
return null;
|
|
78
|
+
const fracMs = (x) => {
|
|
79
|
+
if (!x)
|
|
80
|
+
return 0;
|
|
81
|
+
return Number(`${x}000`.slice(0, 3));
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
fullYear: Number(m[1]),
|
|
85
|
+
monthIndex: Number(m[2]) - 1,
|
|
86
|
+
day: Number(m[3]),
|
|
87
|
+
hour: m[4] === undefined ? 0 : Number(m[4]),
|
|
88
|
+
minute: m[5] === undefined ? 0 : Number(m[5]),
|
|
89
|
+
second: m[6] === undefined ? 0 : Number(m[6]),
|
|
90
|
+
millisecond: fracMs(m[7]),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function toDate(dirtyDate) {
|
|
94
|
+
if (dirtyDate instanceof Date)
|
|
95
|
+
return new Date(dirtyDate.getTime());
|
|
96
|
+
return new Date(dirtyDate);
|
|
97
|
+
}
|
|
98
|
+
function coerceWallCalendarDate(input) {
|
|
99
|
+
if (typeof input !== 'string')
|
|
100
|
+
return toDate(input);
|
|
101
|
+
const t = input.trim();
|
|
102
|
+
let m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(t);
|
|
103
|
+
if (m) {
|
|
104
|
+
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
105
|
+
}
|
|
106
|
+
m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/.exec(t.replace('T', ' '));
|
|
107
|
+
if (m) {
|
|
108
|
+
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0);
|
|
109
|
+
}
|
|
110
|
+
return toDate(new Date(t));
|
|
111
|
+
}
|
|
112
|
+
function truncateToWholeSeconds(ms) {
|
|
113
|
+
const over = ms % 1000;
|
|
114
|
+
return ms - (over >= 0 ? over : over + 1000);
|
|
115
|
+
}
|
|
116
|
+
function localFieldsAsUtcDate(date) {
|
|
117
|
+
return new Date(utcWallMs(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()));
|
|
118
|
+
}
|
|
119
|
+
function calcOffset(date, timezoneString) {
|
|
120
|
+
const tokens = tzTokenizeDate(date, timezoneString);
|
|
121
|
+
const asUTC = utcWallMs(tokens[0], tokens[1] - 1, tokens[2], tokens[3] % 24, tokens[4], tokens[5], 0);
|
|
122
|
+
const asTS = truncateToWholeSeconds(date.getTime());
|
|
123
|
+
return asUTC - asTS;
|
|
124
|
+
}
|
|
125
|
+
function fixOffset(date, offset, timezoneString) {
|
|
126
|
+
const localTs = date.getTime();
|
|
127
|
+
let utcGuess = localTs - offset;
|
|
128
|
+
const o2 = calcOffset(new Date(utcGuess), timezoneString);
|
|
129
|
+
if (offset === o2)
|
|
130
|
+
return offset;
|
|
131
|
+
utcGuess -= o2 - offset;
|
|
132
|
+
const o3 = calcOffset(new Date(utcGuess), timezoneString);
|
|
133
|
+
if (o2 === o3)
|
|
134
|
+
return o2;
|
|
135
|
+
return Math.max(o2, o3);
|
|
136
|
+
}
|
|
137
|
+
function validateTimezoneHour(hours, minutes) {
|
|
138
|
+
return (hours >= -23 && hours <= 23 && (minutes === undefined || (minutes >= 0 && minutes <= 59)));
|
|
139
|
+
}
|
|
140
|
+
function tzParseTimezone(timezoneString, date, isUtcDate) {
|
|
141
|
+
if (!timezoneString)
|
|
142
|
+
return 0;
|
|
143
|
+
if (/^Z$/i.test(timezoneString))
|
|
144
|
+
return 0;
|
|
145
|
+
const hh = /^([+-]\d{2})$/.exec(timezoneString);
|
|
146
|
+
if (hh) {
|
|
147
|
+
const h = parseInt(hh[1], 10);
|
|
148
|
+
if (!validateTimezoneHour(h))
|
|
149
|
+
return NaN;
|
|
150
|
+
return -(h * MS_HOUR);
|
|
151
|
+
}
|
|
152
|
+
const hhmm = /^([+-])(\d{2}):?(\d{2})$/.exec(timezoneString);
|
|
153
|
+
if (hhmm) {
|
|
154
|
+
const h = parseInt(hhmm[2], 10);
|
|
155
|
+
const mins = parseInt(hhmm[3], 10);
|
|
156
|
+
if (!validateTimezoneHour(h, mins))
|
|
157
|
+
return NaN;
|
|
158
|
+
const abs = Math.abs(h) * MS_HOUR + mins * MS_MINUTE;
|
|
159
|
+
return hhmm[1] === '+' ? -abs : abs;
|
|
160
|
+
}
|
|
161
|
+
if (isValidTimezoneIANA(timezoneString)) {
|
|
162
|
+
const utcDate = isUtcDate ? date : localFieldsAsUtcDate(date);
|
|
163
|
+
const offset = calcOffset(utcDate, timezoneString);
|
|
164
|
+
return -(isUtcDate ? offset : fixOffset(date, offset, timezoneString));
|
|
165
|
+
}
|
|
166
|
+
return NaN;
|
|
167
|
+
}
|
|
168
|
+
function utcToZonedTime(dirtyDate, timeZone) {
|
|
169
|
+
const date = toDate(dirtyDate);
|
|
170
|
+
const offsetMilliseconds = tzParseTimezone(timeZone, date, true);
|
|
171
|
+
const d = new Date(date.getTime() - offsetMilliseconds);
|
|
172
|
+
const resultDate = new Date(0);
|
|
173
|
+
resultDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
174
|
+
resultDate.setHours(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
175
|
+
return resultDate;
|
|
176
|
+
}
|
|
177
|
+
function zonedTimeToUtc(dateInput, timeZone) {
|
|
178
|
+
if (typeof dateInput === 'string') {
|
|
179
|
+
const w = wallCalendarPartsFromString(dateInput);
|
|
180
|
+
if (w) {
|
|
181
|
+
const utc = utcWallMs(w.fullYear, w.monthIndex, w.day, w.hour, w.minute, w.second, w.millisecond);
|
|
182
|
+
const offsetMilliseconds = tzParseTimezone(timeZone, new Date(utc), false);
|
|
183
|
+
return new Date(utc + offsetMilliseconds);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const d = coerceWallCalendarDate(dateInput);
|
|
187
|
+
const utc = utcWallMs(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
|
|
188
|
+
const offsetMilliseconds = tzParseTimezone(timeZone, new Date(utc), false);
|
|
189
|
+
return new Date(utc + offsetMilliseconds);
|
|
190
|
+
}
|
package/dist/date.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Единая точка работы с датами в монорепе: локальный разбор, стеночные токены (yyyy-MM-dd…),
|
|
3
|
+
* таймзоны (utcToZonedTime / zonedTimeToUtc), локализованные шаблоны UI (как @alphakits/ui formatDate).
|
|
4
|
+
*/
|
|
5
|
+
export declare function addHours(date: Date, hours: number): Date;
|
|
6
|
+
/** date-fns isBefore(left, right) — left строго раньше right. */
|
|
7
|
+
export declare function isBefore(left: Date, right: Date): boolean;
|
|
8
|
+
/** date-fns differenceInHours (roundingMethod trunc). */
|
|
9
|
+
export declare function differenceInHours(left: Date, right: Date): number;
|
|
10
|
+
/** Локальная дата + календарные дни (без date-fns). */
|
|
11
|
+
export declare function addCalendarDays(date: Date, amount: number): Date;
|
|
12
|
+
export declare function startOfCalendarDay(d: Date): Date;
|
|
13
|
+
/**
|
|
14
|
+
* Приводит значение из URL/API/UI к валидному `Date` (локальная полуночная семантика для `yyyy-MM-dd`).
|
|
15
|
+
* Учитывает Dayjs-подобные объекты (`valueOf`, `toDate`), чтобы не падать на `.getFullYear()`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function coerceToCalendarDate(input: unknown): Date;
|
|
18
|
+
/** Первый день календарного месяца, 00:00 локально (устойчивый аналог date-fns `startOfMonth`). */
|
|
19
|
+
export declare function startOfMonth(input: unknown): Date;
|
|
20
|
+
/** Понедельник ISO-недели, 00:00 локально. */
|
|
21
|
+
export declare function startOfISOWeekCalendar(d: Date): Date;
|
|
22
|
+
/** BCP 47 для i18n languageOnly-кодов (как @alphakits/ui). */
|
|
23
|
+
export declare const DISPLAY_DATE_LOCALE_BY_LANG: Record<string, string>;
|
|
24
|
+
export declare function resolveDisplayLocaleTag(lang?: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Разбор фиксированных шаблонов UI (локальное время), как в @alphakits/ui `parse`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCalendarPattern(value: string, pattern: string): Date;
|
|
29
|
+
/**
|
|
30
|
+
* Локальный разбор `yyyy-MM-dd` / `yyyy-MM-dd HH:mm` (строгое совпадение строки).
|
|
31
|
+
* Реализация через `parseCalendarPattern`; неподдерживаемый pattern → NaN.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseDate(value: string, pattern: string): Date;
|
|
34
|
+
export type FormatLocalizedDatePatternOptions = {
|
|
35
|
+
locale?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Локализованные токены: yyyy, yy, MM, dd, HH, mm, MMM, MMMM, LLL, LLLL, EEE, EEEE, EEEEEE, iii.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatLocalizedDatePattern(d: Date, pattern: string, options?: FormatLocalizedDatePatternOptions): string;
|
|
41
|
+
export type FormatDateLocaleOptions = {
|
|
42
|
+
time?: string;
|
|
43
|
+
format?: string;
|
|
44
|
+
lang?: string;
|
|
45
|
+
};
|
|
46
|
+
export { utcToZonedTime, zonedTimeToUtc } from './date-timezone';
|
|
47
|
+
/**
|
|
48
|
+
* Единая точка форматирования дат.
|
|
49
|
+
* - `formatDate(date, pattern)` — числовые токены в локальной стенке (`yyyy`, `MM`, `dd`, `HH`, `mm`, `ss`).
|
|
50
|
+
* - `formatDate(input, { format, lang, time })` — как `formatDate` в @alphakits/ui (локализованные MMMM, слоты и т.д.).
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatDate(date: Date, pattern: string): string;
|
|
53
|
+
export declare function formatDate(date: Date | number | string, options: FormatDateLocaleOptions): string;
|
package/dist/date.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Единая точка работы с датами в монорепе: локальный разбор, стеночные токены (yyyy-MM-dd…),
|
|
4
|
+
* таймзоны (utcToZonedTime / zonedTimeToUtc), локализованные шаблоны UI (как @alphakits/ui formatDate).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.zonedTimeToUtc = exports.utcToZonedTime = exports.DISPLAY_DATE_LOCALE_BY_LANG = void 0;
|
|
8
|
+
exports.addHours = addHours;
|
|
9
|
+
exports.isBefore = isBefore;
|
|
10
|
+
exports.differenceInHours = differenceInHours;
|
|
11
|
+
exports.addCalendarDays = addCalendarDays;
|
|
12
|
+
exports.startOfCalendarDay = startOfCalendarDay;
|
|
13
|
+
exports.coerceToCalendarDate = coerceToCalendarDate;
|
|
14
|
+
exports.startOfMonth = startOfMonth;
|
|
15
|
+
exports.startOfISOWeekCalendar = startOfISOWeekCalendar;
|
|
16
|
+
exports.resolveDisplayLocaleTag = resolveDisplayLocaleTag;
|
|
17
|
+
exports.parseCalendarPattern = parseCalendarPattern;
|
|
18
|
+
exports.parseDate = parseDate;
|
|
19
|
+
exports.formatLocalizedDatePattern = formatLocalizedDatePattern;
|
|
20
|
+
exports.formatDate = formatDate;
|
|
21
|
+
const MS_HOUR = 3600000;
|
|
22
|
+
function pad2(n) {
|
|
23
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
24
|
+
}
|
|
25
|
+
function addHours(date, hours) {
|
|
26
|
+
const d = new Date(date);
|
|
27
|
+
d.setHours(d.getHours() + hours);
|
|
28
|
+
return d;
|
|
29
|
+
}
|
|
30
|
+
/** date-fns isBefore(left, right) — left строго раньше right. */
|
|
31
|
+
function isBefore(left, right) {
|
|
32
|
+
return left.getTime() < right.getTime();
|
|
33
|
+
}
|
|
34
|
+
/** date-fns differenceInHours (roundingMethod trunc). */
|
|
35
|
+
function differenceInHours(left, right) {
|
|
36
|
+
const diff = (left.getTime() - right.getTime()) / MS_HOUR;
|
|
37
|
+
return diff < 0 ? Math.ceil(diff) : Math.floor(diff);
|
|
38
|
+
}
|
|
39
|
+
/** Локальная дата + календарные дни (без date-fns). */
|
|
40
|
+
function addCalendarDays(date, amount) {
|
|
41
|
+
const d = new Date(date);
|
|
42
|
+
d.setDate(d.getDate() + amount);
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
function startOfCalendarDay(d) {
|
|
46
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Приводит значение из URL/API/UI к валидному `Date` (локальная полуночная семантика для `yyyy-MM-dd`).
|
|
50
|
+
* Учитывает Dayjs-подобные объекты (`valueOf`, `toDate`), чтобы не падать на `.getFullYear()`.
|
|
51
|
+
*/
|
|
52
|
+
function coerceToCalendarDate(input) {
|
|
53
|
+
if (input == null || input === '') {
|
|
54
|
+
return new Date();
|
|
55
|
+
}
|
|
56
|
+
if (input instanceof Date) {
|
|
57
|
+
const t = input.getTime();
|
|
58
|
+
return Number.isNaN(t) ? new Date() : new Date(t);
|
|
59
|
+
}
|
|
60
|
+
if (typeof input === 'number' && Number.isFinite(input)) {
|
|
61
|
+
const d = new Date(input);
|
|
62
|
+
return Number.isNaN(d.getTime()) ? new Date() : d;
|
|
63
|
+
}
|
|
64
|
+
if (typeof input === 'string') {
|
|
65
|
+
const trimmed = input.trim();
|
|
66
|
+
if (!trimmed) {
|
|
67
|
+
return new Date();
|
|
68
|
+
}
|
|
69
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
70
|
+
return parseCalendarPattern(trimmed, 'yyyy-MM-dd');
|
|
71
|
+
}
|
|
72
|
+
const d = new Date(trimmed);
|
|
73
|
+
return Number.isNaN(d.getTime()) ? new Date() : d;
|
|
74
|
+
}
|
|
75
|
+
if (typeof input === 'object') {
|
|
76
|
+
const o = input;
|
|
77
|
+
if (typeof o.toDate === 'function') {
|
|
78
|
+
try {
|
|
79
|
+
const converted = o.toDate();
|
|
80
|
+
if (converted instanceof Date) {
|
|
81
|
+
const t = converted.getTime();
|
|
82
|
+
return Number.isNaN(t) ? new Date() : new Date(t);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* ignore */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (typeof o.valueOf === 'function') {
|
|
90
|
+
const v = Number(o.valueOf());
|
|
91
|
+
if (Number.isFinite(v)) {
|
|
92
|
+
const d = new Date(v);
|
|
93
|
+
return Number.isNaN(d.getTime()) ? new Date() : d;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return new Date();
|
|
98
|
+
}
|
|
99
|
+
/** Первый день календарного месяца, 00:00 локально (устойчивый аналог date-fns `startOfMonth`). */
|
|
100
|
+
function startOfMonth(input) {
|
|
101
|
+
const d = coerceToCalendarDate(input);
|
|
102
|
+
return new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0, 0);
|
|
103
|
+
}
|
|
104
|
+
/** Понедельник ISO-недели, 00:00 локально. */
|
|
105
|
+
function startOfISOWeekCalendar(d) {
|
|
106
|
+
const day = startOfCalendarDay(d);
|
|
107
|
+
const dow = day.getDay();
|
|
108
|
+
const delta = dow === 0 ? -6 : 1 - dow;
|
|
109
|
+
return addCalendarDays(day, delta);
|
|
110
|
+
}
|
|
111
|
+
/** BCP 47 для i18n languageOnly-кодов (как @alphakits/ui). */
|
|
112
|
+
exports.DISPLAY_DATE_LOCALE_BY_LANG = {
|
|
113
|
+
ru: 'ru-RU',
|
|
114
|
+
de: 'de-DE',
|
|
115
|
+
en: 'en-GB',
|
|
116
|
+
pl: 'pl-PL',
|
|
117
|
+
et: 'et-EE',
|
|
118
|
+
fr: 'fr-FR',
|
|
119
|
+
it: 'it-IT',
|
|
120
|
+
pt: 'pt-PT',
|
|
121
|
+
es: 'es-ES',
|
|
122
|
+
el: 'el-GR',
|
|
123
|
+
nl: 'nl-NL',
|
|
124
|
+
cs: 'cs-CZ',
|
|
125
|
+
fi: 'fi-FI',
|
|
126
|
+
da: 'da-DK',
|
|
127
|
+
sk: 'sk-SK',
|
|
128
|
+
hu: 'hu-HU',
|
|
129
|
+
sv: 'sv-SE',
|
|
130
|
+
lv: 'lv-LV',
|
|
131
|
+
bg: 'bg-BG',
|
|
132
|
+
ro: 'ro-RO',
|
|
133
|
+
lt: 'lt-LT',
|
|
134
|
+
he: 'he-IL',
|
|
135
|
+
};
|
|
136
|
+
function resolveDisplayLocaleTag(lang) {
|
|
137
|
+
const key = lang?.split('-')[0]?.toLowerCase() || 'en';
|
|
138
|
+
return exports.DISPLAY_DATE_LOCALE_BY_LANG[key] ?? 'en-GB';
|
|
139
|
+
}
|
|
140
|
+
function isNumericMonthValue(value) {
|
|
141
|
+
return /^\d+\.?$/.test(value.trim());
|
|
142
|
+
}
|
|
143
|
+
function intlStandaloneMonth(d, locale, style) {
|
|
144
|
+
const value = new Intl.DateTimeFormat(locale, { month: style }).format(d);
|
|
145
|
+
if (style === 'short' && isNumericMonthValue(value)) {
|
|
146
|
+
return new Intl.DateTimeFormat(locale, { month: 'long' }).format(d);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function intlMonthInDateContext(d, locale, style) {
|
|
151
|
+
const parts = new Intl.DateTimeFormat(locale, { day: 'numeric', month: style }).formatToParts(d);
|
|
152
|
+
const month = parts.find((p) => p.type === 'month')?.value;
|
|
153
|
+
if (month && !isNumericMonthValue(month)) {
|
|
154
|
+
return month;
|
|
155
|
+
}
|
|
156
|
+
return intlStandaloneMonth(d, locale, style);
|
|
157
|
+
}
|
|
158
|
+
function intlWeekdayPart(d, locale, style) {
|
|
159
|
+
const parts = new Intl.DateTimeFormat(locale, { weekday: style }).formatToParts(d);
|
|
160
|
+
const w = parts.find((p) => p.type === 'weekday')?.value;
|
|
161
|
+
return w ?? new Intl.DateTimeFormat(locale, { weekday: style }).format(d);
|
|
162
|
+
}
|
|
163
|
+
function intlWeekdayVeryShort(d, locale) {
|
|
164
|
+
return intlWeekdayPart(d, locale, 'short').slice(0, 2);
|
|
165
|
+
}
|
|
166
|
+
function applyCalendarTime(d, timePart) {
|
|
167
|
+
if (!timePart)
|
|
168
|
+
return;
|
|
169
|
+
const [hh, mm] = timePart.split(':').map((x) => Number(x));
|
|
170
|
+
d.setHours(Number.isFinite(hh) ? hh : 0, Number.isFinite(mm) ? mm : 0, 0, 0);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Разбор фиксированных шаблонов UI (локальное время), как в @alphakits/ui `parse`.
|
|
174
|
+
*/
|
|
175
|
+
function parseCalendarPattern(value, pattern) {
|
|
176
|
+
if (pattern === 'yyyy-MM-dd HH:mm' || pattern === 'dd.MM.yyyy HH:mm') {
|
|
177
|
+
const [datePart, timePart] = value.trim().split(/\s+/);
|
|
178
|
+
const dateOnlyPattern = pattern.replace(/\s+HH:mm$/, '');
|
|
179
|
+
const parsed = parseCalendarPattern(datePart, dateOnlyPattern);
|
|
180
|
+
applyCalendarTime(parsed, timePart);
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
if (pattern === 'yyyy-MM-dd') {
|
|
184
|
+
const [ys, ms, ds] = value.split('-');
|
|
185
|
+
return new Date(Number(ys), Number(ms) - 1, Number(ds), 0, 0, 0, 0);
|
|
186
|
+
}
|
|
187
|
+
if (pattern === 'dd.MM.yyyy') {
|
|
188
|
+
const [ds, ms, ys] = value.split('.');
|
|
189
|
+
return new Date(Number(ys), Number(ms) - 1, Number(ds), 0, 0, 0, 0);
|
|
190
|
+
}
|
|
191
|
+
if (pattern === 'dd.MM.yy') {
|
|
192
|
+
const [ds, ms, ys] = value.split('.');
|
|
193
|
+
let year = Number(ys);
|
|
194
|
+
if (year < 100) {
|
|
195
|
+
year += year >= 70 ? 1900 : 2000;
|
|
196
|
+
}
|
|
197
|
+
return new Date(year, Number(ms) - 1, Number(ds), 0, 0, 0, 0);
|
|
198
|
+
}
|
|
199
|
+
return new Date(value);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Локальный разбор `yyyy-MM-dd` / `yyyy-MM-dd HH:mm` (строгое совпадение строки).
|
|
203
|
+
* Реализация через `parseCalendarPattern`; неподдерживаемый pattern → NaN.
|
|
204
|
+
*/
|
|
205
|
+
function parseDate(value, pattern) {
|
|
206
|
+
const s = value.trim();
|
|
207
|
+
if (pattern !== 'yyyy-MM-dd' && pattern !== 'yyyy-MM-dd HH:mm') {
|
|
208
|
+
return new Date(NaN);
|
|
209
|
+
}
|
|
210
|
+
if (pattern === 'yyyy-MM-dd' && !/^(\d{4})-(\d{2})-(\d{2})$/.exec(s)) {
|
|
211
|
+
return new Date(NaN);
|
|
212
|
+
}
|
|
213
|
+
if (pattern === 'yyyy-MM-dd HH:mm' &&
|
|
214
|
+
!/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/.exec(s)) {
|
|
215
|
+
return new Date(NaN);
|
|
216
|
+
}
|
|
217
|
+
return parseCalendarPattern(s, pattern);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Локализованные токены: yyyy, yy, MM, dd, HH, mm, MMM, MMMM, LLL, LLLL, EEE, EEEE, EEEEEE, iii.
|
|
221
|
+
*/
|
|
222
|
+
function formatLocalizedDatePattern(d, pattern, options) {
|
|
223
|
+
const dt = d instanceof Date
|
|
224
|
+
? new Date(d.getTime())
|
|
225
|
+
: typeof d === 'number' && Number.isFinite(d)
|
|
226
|
+
? new Date(d)
|
|
227
|
+
: typeof d === 'string'
|
|
228
|
+
? new Date(d)
|
|
229
|
+
: new Date(NaN);
|
|
230
|
+
if (Number.isNaN(dt.getTime())) {
|
|
231
|
+
return 'Invalid date';
|
|
232
|
+
}
|
|
233
|
+
const locale = options?.locale ?? 'en-GB';
|
|
234
|
+
const y = dt.getFullYear();
|
|
235
|
+
const m = dt.getMonth();
|
|
236
|
+
const day = dt.getDate();
|
|
237
|
+
const h = dt.getHours();
|
|
238
|
+
const minu = dt.getMinutes();
|
|
239
|
+
// MMM/MMMM зависят от наличия дня в шаблоне:
|
|
240
|
+
// c `dd` — форма «в дате» (родительный падеж в ru: «2 мая»),
|
|
241
|
+
// без `dd` — стандартная форма («Май», «Сент»), без числовой деградации в локалях вроде cs/sk/hu.
|
|
242
|
+
const hasDayToken = /dd/.test(pattern);
|
|
243
|
+
const monthShort = () => hasDayToken
|
|
244
|
+
? intlMonthInDateContext(dt, locale, 'short')
|
|
245
|
+
: intlStandaloneMonth(dt, locale, 'short');
|
|
246
|
+
const monthLong = () => hasDayToken
|
|
247
|
+
? intlMonthInDateContext(dt, locale, 'long')
|
|
248
|
+
: intlStandaloneMonth(dt, locale, 'long');
|
|
249
|
+
const tokenReplacements = [
|
|
250
|
+
[/yyyy/g, () => String(y)],
|
|
251
|
+
[/LLLL/g, () => intlStandaloneMonth(dt, locale, 'long')],
|
|
252
|
+
[/MMMM/g, () => monthLong()],
|
|
253
|
+
[/LLL/g, () => intlStandaloneMonth(dt, locale, 'short')],
|
|
254
|
+
[/MMM/g, () => monthShort()],
|
|
255
|
+
[/EEEEEE/g, () => intlWeekdayVeryShort(dt, locale)],
|
|
256
|
+
[/EEEE/g, () => intlWeekdayPart(dt, locale, 'long')],
|
|
257
|
+
[/EEE/g, () => intlWeekdayPart(dt, locale, 'short')],
|
|
258
|
+
[/iii/g, () => intlWeekdayPart(dt, locale, 'short')],
|
|
259
|
+
[/MM/g, () => pad2(m + 1)],
|
|
260
|
+
[/dd/g, () => pad2(day)],
|
|
261
|
+
[/HH/g, () => pad2(h)],
|
|
262
|
+
[/mm/g, () => pad2(minu)],
|
|
263
|
+
[/yy/g, () => pad2(y % 100)],
|
|
264
|
+
];
|
|
265
|
+
let out = pattern;
|
|
266
|
+
tokenReplacements.forEach(([re, fn]) => {
|
|
267
|
+
out = out.replace(re, fn);
|
|
268
|
+
});
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
function formatDateForLocaleInput(date, { time, format: fmt = 'dd MMMM yyyy, HH:mm', lang }) {
|
|
272
|
+
if (date == null || date === '')
|
|
273
|
+
return '-';
|
|
274
|
+
const localeTag = resolveDisplayLocaleTag(lang);
|
|
275
|
+
let d;
|
|
276
|
+
try {
|
|
277
|
+
if (typeof date === 'number' && Number.isFinite(date)) {
|
|
278
|
+
d = new Date(date);
|
|
279
|
+
}
|
|
280
|
+
else if (date instanceof Date) {
|
|
281
|
+
d = new Date(date.getTime());
|
|
282
|
+
}
|
|
283
|
+
else if (typeof date === 'string' &&
|
|
284
|
+
(date.includes('Z') || /^\d{4}-\d{2}-\d{2}T/.test(date))) {
|
|
285
|
+
d = new Date(date);
|
|
286
|
+
}
|
|
287
|
+
else if (typeof date === 'string') {
|
|
288
|
+
const split = date.split('-');
|
|
289
|
+
let dateFormat = split.length === 3 ? 'yyyy-MM-dd' : 'dd.MM.yyyy';
|
|
290
|
+
const formattedTime = time ? ` ${time.split(':')[0]}:${time.split(':')[1]}` : '';
|
|
291
|
+
if (formattedTime)
|
|
292
|
+
dateFormat += ' HH:mm';
|
|
293
|
+
d = parseCalendarPattern(`${date}${formattedTime}`, dateFormat);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
d = new Date(NaN);
|
|
297
|
+
}
|
|
298
|
+
if (Number.isNaN(d.getTime()))
|
|
299
|
+
return `Invalid date ${date}`;
|
|
300
|
+
return formatLocalizedDatePattern(d, fmt, { locale: localeTag });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return `Invalid date ${date}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Поддержка токенов yyyy MM dd HH mm ss и литералов (другие символы копируются). Только для `Date`. */
|
|
307
|
+
function formatWallClockPattern(date, pattern) {
|
|
308
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
309
|
+
throw new TypeError('formatWallClockPattern: invalid Date');
|
|
310
|
+
}
|
|
311
|
+
const y = date.getFullYear();
|
|
312
|
+
const mo = date.getMonth() + 1;
|
|
313
|
+
const d = date.getDate();
|
|
314
|
+
const H = date.getHours();
|
|
315
|
+
const mi = date.getMinutes();
|
|
316
|
+
const sec = date.getSeconds();
|
|
317
|
+
let out = '';
|
|
318
|
+
let i = 0;
|
|
319
|
+
while (i < pattern.length) {
|
|
320
|
+
if (pattern.startsWith('yyyy', i)) {
|
|
321
|
+
out += y;
|
|
322
|
+
i += 4;
|
|
323
|
+
}
|
|
324
|
+
else if (pattern.startsWith('MM', i)) {
|
|
325
|
+
out += pad2(mo);
|
|
326
|
+
i += 2;
|
|
327
|
+
}
|
|
328
|
+
else if (pattern.startsWith('dd', i)) {
|
|
329
|
+
out += pad2(d);
|
|
330
|
+
i += 2;
|
|
331
|
+
}
|
|
332
|
+
else if (pattern.startsWith('HH', i)) {
|
|
333
|
+
out += pad2(H);
|
|
334
|
+
i += 2;
|
|
335
|
+
}
|
|
336
|
+
else if (pattern.startsWith('mm', i)) {
|
|
337
|
+
out += pad2(mi);
|
|
338
|
+
i += 2;
|
|
339
|
+
}
|
|
340
|
+
else if (pattern.startsWith('ss', i)) {
|
|
341
|
+
out += pad2(sec);
|
|
342
|
+
i += 2;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
out += pattern[i];
|
|
346
|
+
i += 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
var date_timezone_1 = require("./date-timezone");
|
|
352
|
+
Object.defineProperty(exports, "utcToZonedTime", { enumerable: true, get: function () { return date_timezone_1.utcToZonedTime; } });
|
|
353
|
+
Object.defineProperty(exports, "zonedTimeToUtc", { enumerable: true, get: function () { return date_timezone_1.zonedTimeToUtc; } });
|
|
354
|
+
function formatDate(date, patternOrOptions) {
|
|
355
|
+
if (typeof patternOrOptions === 'string') {
|
|
356
|
+
if (!(date instanceof Date)) {
|
|
357
|
+
throw new TypeError('formatDate(date, pattern): first argument must be a Date');
|
|
358
|
+
}
|
|
359
|
+
return formatWallClockPattern(date, patternOrOptions);
|
|
360
|
+
}
|
|
361
|
+
return formatDateForLocaleInput(date, patternOrOptions);
|
|
362
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const date_1 = require("./date");
|
|
4
|
+
describe('formatLocalizedDatePattern', () => {
|
|
5
|
+
const nov23 = new Date(2024, 10, 23, 15, 5);
|
|
6
|
+
it('uses genitive month in ru for MMMM with separate dd', () => {
|
|
7
|
+
const s = (0, date_1.formatLocalizedDatePattern)(nov23, 'dd MMMM', { locale: 'ru-RU' });
|
|
8
|
+
expect(s).toBe('23 ноября');
|
|
9
|
+
});
|
|
10
|
+
it('uses nominative standalone month for LLLL in ru', () => {
|
|
11
|
+
const s = (0, date_1.formatLocalizedDatePattern)(nov23, 'LLLL', { locale: 'ru-RU' });
|
|
12
|
+
expect(s).toBe('ноябрь');
|
|
13
|
+
});
|
|
14
|
+
it('uses standalone short month for MMM without dd in ru', () => {
|
|
15
|
+
const may2 = new Date(2024, 4, 2);
|
|
16
|
+
const s = (0, date_1.formatLocalizedDatePattern)(may2, 'MMM', { locale: 'ru-RU' });
|
|
17
|
+
expect(s).not.toMatch(/^\d+\.?$/);
|
|
18
|
+
expect(s.toLowerCase()).toContain('ма');
|
|
19
|
+
});
|
|
20
|
+
it('uses contextual short month for dd MMM in ru', () => {
|
|
21
|
+
const may2 = new Date(2024, 4, 2);
|
|
22
|
+
const s = (0, date_1.formatLocalizedDatePattern)(may2, 'dd MMM', { locale: 'ru-RU' });
|
|
23
|
+
expect(s).toMatch(/^02\s+\S+/);
|
|
24
|
+
expect(s).not.toMatch(/\d+\.?$/);
|
|
25
|
+
});
|
|
26
|
+
it('returns textual month for MMM in cs (no numeric fallback)', () => {
|
|
27
|
+
const may2 = new Date(2024, 4, 2);
|
|
28
|
+
const s = (0, date_1.formatLocalizedDatePattern)(may2, 'MMM', { locale: 'cs-CZ' });
|
|
29
|
+
expect(s).not.toMatch(/^\d+\.?$/);
|
|
30
|
+
});
|
|
31
|
+
it('returns textual month for dd MMM in cs (no numeric fallback)', () => {
|
|
32
|
+
const may2 = new Date(2024, 4, 2);
|
|
33
|
+
const s = (0, date_1.formatLocalizedDatePattern)(may2, 'dd MMM', { locale: 'cs-CZ' });
|
|
34
|
+
expect(s.replace(/^02\s*/, '')).not.toMatch(/^\d+\.?$/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('formatDate (locale overload)', () => {
|
|
38
|
+
it('parses yyyy-MM-dd with time like @alphakits/ui', () => {
|
|
39
|
+
const s = (0, date_1.formatDate)('2024-11-23', {
|
|
40
|
+
lang: 'ru',
|
|
41
|
+
time: '14:30',
|
|
42
|
+
format: 'dd MMMM, HH:mm',
|
|
43
|
+
});
|
|
44
|
+
expect(s).toContain('ноября');
|
|
45
|
+
expect(s).toContain('14:30');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('formatDate (wall-clock pattern)', () => {
|
|
49
|
+
it('formats numeric pattern', () => {
|
|
50
|
+
const d = new Date(2024, 10, 23, 14, 5, 9);
|
|
51
|
+
expect((0, date_1.formatDate)(d, 'yyyy-MM-dd HH:mm:ss')).toBe('2024-11-23 14:05:09');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('parseCalendarPattern', () => {
|
|
55
|
+
it('parses dd.MM.yyyy', () => {
|
|
56
|
+
const d = (0, date_1.parseCalendarPattern)('23.11.2024', 'dd.MM.yyyy');
|
|
57
|
+
expect(d.getFullYear()).toBe(2024);
|
|
58
|
+
expect(d.getMonth()).toBe(10);
|
|
59
|
+
expect(d.getDate()).toBe(23);
|
|
60
|
+
});
|
|
61
|
+
});
|